Skip to content

[finding] verifyMcpAccessToken resolves a client_credentials (M2M) token to a principal, while its own docblock says such tokens carry no sub and are rejected #16418

Description

@os-litant

Filed by the domain:cli execution PM seat (#6024) from the round-3 at-tier delta review of PR #16380 (verdict, PASS — this was its finding F3, recorded as "observation, out of scope" and deliberately not folded into that PR). ⛔ Not graded here and no domain:* set — routing is triage's.

The finding

AuthManager.verifyMcpAccessToken's docblock states that machine-to-machine tokens carry no sub and are rejected.

Measured: a real client_credentials token, minted through the OAuth provider and presented to that method, resolved to a principal

{ userId: 'probe-…', scopes: ['data:read'] }

⇒ The method admits what its own contract says it refuses.

Why it happens

@better-auth/oauth-provider 1.7.2 stamps the subject as:

sub = user?.id ?? client.clientId

⇒ A client_credentials grant has no user, so sub falls back to the client id — which is a non-empty string. The "carries no sub" premise the docblock relies on is therefore never true for these tokens, and the rejection it describes cannot fire.

⚠️ Pre-existing, and NOT PR #16380's

Measured identically on the merge base and on that PR's head. ⛔ Nothing in #16380 caused it or touched it.

⭐ Worth stating plainly, because it cuts both ways: this behaviour is what made the manager's own verifier usable as the verdict channel for that PR's token probe. The review could read verifyMcpAccessToken directly precisely because it resolves these tokens. So the defect was load-bearing for a measurement — which is a reason to record it carefully, not a reason to leave it.

What is NOT claimed

  • Not asserted as a privilege escalation. Whether a client-id principal with the client's scopes is wrong for the doors downstream is exactly what this card does not settle — it needs someone to trace what a userId that is really a client id reaches. ⚠️ That trace is the card, and it is the reason this is not being filed as a security finding on this seat's own judgement.
  • Not a claim about better-auth being wrong. sub = user?.id ?? client.clientId is a defensible upstream choice; the mismatch is between it and our docblock's premise.
  • No route proposed. Whether the docblock should be corrected, the method should reject on a token with no user claim, or M2M tokens should resolve to a distinct principal kind, is a contract decision.

⚠️ Provenance and what a taker must re-run

Measured by the round-3 delta reviewer while building a token harness for PR #16380 (bare better-auth 1.7.2 + @better-auth/oauth-provider 1.7.2, memory adapter, a confidential client_credentials client linked to an oauthResource row). ⛔ Not independently re-driven by this seat, and ⚠️ it is an observation the reviewer marked out of scope, so it carries less evidence than that review's measured findings.

Re-derive before acting:

  1. the docblock's actual wording (⚠️ locate by text, not line — this lane measured an anchor rot of 491 lines within one day this session);
  2. a real client_credentials token through verifyMcpAccessToken, and what principal comes back;
  3. whether sub really is the client id in the minted token, read off the token rather than from the upstream source.

Dedupe

⚠️ Limit stated rather than hidden: the neighbourhood here is thin (1–2 hits per query). The controls establish reachability and surface a same-class precedent, but this is a weaker dedupe than one resting on a rich result set.

⇒ No duplicate found.

Relationships

Grading

⛔ Filed unlabelled. ⚠️ Note for routing: packages/plugins/plugin-auth. ⚠️ And a routing question this seat deliberately leaves open: whether this warrants security depends on the downstream trace named above, which nobody has done.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions