Insecure cryptographic defaults in oidcProvider: alg=none advertised and plain PKCE accepted by default
GHSA advisory for better-auth (< 1.6.11) documenting that the deprecated oidcProvider() and mcp() plugins publish "none" as a supported ID-token signing algorithm in their OIDC discovery document and default allowPlainCodeChallengeMethod to true, silently rewriting missing code_challenge_method to "plain" — both violating OAuth 2.1/RFC 9700. The fix removes "none" from the metadata, defaults plain PKCE to disabled, and rejects requests missing code_challenge_method instead of downgrading them; @better-auth/oauth-provider was never affected.
What it is
GHSA-9h47-pqcx-hjr4 covers two insecure cryptographic defaults in better-auth's legacy oidcProvider() plugin (and the mcp() plugin, which delegates to it and inherits both defaults). The OIDC discovery document advertises "none" as a supported id_token_signing_alg, and PKCE plain is accepted by default. Fixed in better-auth@1.6.11.
How it works
Two distinct defects:
- alg=none advertised. The metadata builders unconditionally inject
"none"intoid_token_signing_alg_values_supported(and, formcp, intoresource_signing_alg_values_supportedon the OAuth protected-resource metadata). A relying party whose JWT library negotiates the algorithm from discovery without pinning can be steered into accepting unsigned tokens. - plain PKCE accepted and silently defaulted. The authorize handler accepts
code_challenge_method=plain, and a missingcode_challenge_methodis rewritten to"plain"before the allowlist check runs — so even a client that strips the parameter lands in the plain path. This directly contradicts discovery, which advertisescode_challenge_methods_supported: ["S256"]. RFC 9700 §2.1.1 (OAuth 2.1) forbids plain.
Preconditions / Affected
All must be true: better-auth below 1.6.11, and oidcProvider() or mcp() enabled. Then:
- alg-none impact requires a relying party that negotiates the signing alg from discovery without pinning.
- PKCE impact requires the authorization URL to leak to any party other than the user agent and the OP (Referer headers, browser history, screen capture, proxy logs), enabling authorization-code interception.
Users of @better-auth/oauth-provider (the canonical replacement) who never enabled the legacy plugins are not affected — its metadata type union structurally excludes none, and its authorize schema is z.literal("S256").optional(), rejecting plain at parse time.
What to do
Upgrade to better-auth@1.6.11 or later, and migrate to @better-auth/oauth-provider when feasible. The patch drops none from both alg lists, defaults allowPlainCodeChallengeMethod to false, and rejects a code_challenge sent without an accompanying code_challenge_method instead of rewriting it to plain.
If you cannot upgrade:
- Disable plain PKCE explicitly:
oidcProvider({ allowPlainCodeChallengeMethod: false }) (equivalent on mcp). Note: this closes runtime acceptance of plain but the silent downgrade of a missing method still rewrites it.
- Override the metadata to drop
none:
oidcProvider({ metadata: { id_token_signing_alg_values_supported: ["RS256"] } }) For mcp, set the same on options.oidcConfig.metadata. Verify by curling the .well-known endpoint.
Caveats
The workaround for plain PKCE is partial: with allowPlainCodeChallengeMethod: false on an unpatched version, a request that omits code_challenge_method is still silently treated as plain — only the patch (1.6.11) rejects that case. Integrators needing plain for legacy clients can re-enable it post-patch with allowPlainCodeChallengeMethod: true, which restores the silent-downgrade behavior for everyone using that instance.
Organisations named here
- PKCE
- RFC 9700
Technologies named here
- better-auth
- oidcProvider plugin
- mcp plugin
- @better-auth/oauth-provider
- OIDC
- OAuth
- OAuth 2.1
Techniques named here
- Unsigned tokens via alg=none
- Plain PKCE downgrade
Related in Identity & Access
Education Under Attack: The Pattern Behind Recent University Breaches
Next-Gen Phishing Tactics Users Aren’t Ready For | Huntress
.png)
Google API keys keep working after you delete them
