Skip to content

String-typed role claims silently get the fallback role #6305

Description

@Yanhaoxi

Bug description

pkg/auth/awssts maps JWT claims to IAM roles. A claim-based mapping is documented and tested as an exact claim-value match (config.go:74-77, role_mapper_test.go list cases), but each mapping is evaluated with the fixed CEL expression claim_value in claims[role_claim_key] (role_mapper.go:23). claims is declared map<string, dyn> (role_mapper.go:40), so the right operand's runtime type decides in's semantics, and no normalization exists in the path.

In the pinned cel-go v0.30.0, in has only list/map overloads — no string overload. When an IdP emits the role claim as a string (e.g. groups: "admins", or any string-valued claim used as RoleClaim like azp/aud), evaluation raises a runtime error. SelectRole swallows the error with a Debug-only log and treats the mapping as "no match" (role_mapper.go:184-189), then returns FallbackRoleArn (role_mapper.go:196-202).

Steps to reproduce

Verified on db86c06b (Go 1.26.5). groups: "admins" (string) with Claim: "admins" should select AdminRole:

cfg := &awssts.Config{
    Region:          "us-east-1",
    RoleClaim:       "groups",
    FallbackRoleArn: "arn:aws:iam::123456789012:role/FallbackRole",
    RoleMappings: []awssts.RoleMapping{
        {Claim: "admins", RoleArn: "arn:aws:iam::123456789012:role/AdminRole", Priority: intPtr(1)},
    },
}
rm, _ := awssts.NewRoleMapper(cfg)
role, _ := rm.SelectRole(map[string]any{"sub": "u1", "groups": "admins"}) // returns FallbackRole, want AdminRole
go test -v ./pkg/auth/awssts -run TestRoleMapper_SelectRole -count=1

Observed: groups="admins" (string) → FallbackRole, with the underlying CEL evaluation failing (no such overload); control groups=["admins"] (list) → AdminRole.

Expected behavior

A string-typed role claim equal to the configured Claim (e.g. groups: "admins" for Claim: "admins") must select the mapped role; superadmins / admins-readonly must not. Evaluation failures for unsupported claim shapes must not be silently treated as "no match".

Actual behavior

"admins" in "admins" raises a CEL runtime error; the mapping is skipped with a Debug-only log and the user is granted FallbackRoleArn — silently.

Environment

  • Go 1.26.5; toolhive db86c06b (v0.42.1-23-gdb86c06b)
  • github.com/google/cel-go v0.30.0 (go.mod:29) — in registers only list/map overloads
  • Reachable on validated JWT claims via the vMCP aws_sts strategy (pkg/vmcp/auth/strategies/aws_sts.go:129,170) and the standalone middleware (pkg/auth/awssts/middleware.go:172)

Additional context

  • Impact: a user whose role claim is a single string never gets their mapped role. If FallbackRoleArn is at least as privileged as a mapped role, this is a silent authorization bypass; otherwise it is a silent wrong-role grant. No operator-visible signal is emitted.
  • Suggested fix: normalize a string-typed role claim to a single-element list before evaluation (restoring exact membership), and fail closed (Warn/Error log + error return) on evaluation failure instead of falling back. Verified against go test ./pkg/auth/awssts/... ./pkg/vmcp/auth/strategies/... — no existing test regresses (all list-claim tests are unaffected).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds-triageIssue needs initial triage by a maintainer

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions