Insecure Crypto Defaults in better-auth OIDC
CVE-2026-67336: better-auth before 1.6.11 allows unsigned JWT tokens and plain PKCE in OIDC and MCP plugins, enabling token forgery and auth code interception.
Overview
CVE-2026-67336 is a HIGH-severity vulnerability affecting better-auth, a popular TypeScript authentication framework, in all versions prior to 1.6.11. The flaw resides in the oidcProvider and mcp (Model Context Protocol) plugins, both of which ship with insecure cryptographic defaults: they advertise the none algorithm in their supported algorithm lists and accept plain PKCE code challenges by default, rather than enforcing the cryptographically sound S256 method.
These are not configuration edge cases — they are defaults. Any application that enables these plugins without explicit hardening is silently exposed. The none algorithm issue is a well-documented class of JWT attack that has burned production systems repeatedly since it was first documented against early JOSE library implementations. Its reappearance in a modern, actively maintained auth library signals a broader pattern of developers treating algorithm agility as a feature rather than an attack surface.
The vulnerability affects any service acting as an OIDC provider using better-auth, including single-page applications, API gateways, and agentic AI backends leveraging the MCP plugin. Given the framework’s growing adoption in the Node.js and edge-runtime ecosystem, the blast radius is non-trivial.
Technical Analysis
The root cause is twofold and stems from how the plugins register their supported algorithm sets and PKCE methods during provider initialization.
JWT Algorithm Negotiation (none inclusion)
OIDC providers expose a discovery document at /.well-known/openid-configuration which advertises, among other things, the id_token_signing_alg_values_supported array. When better-auth’s oidcProvider plugin constructs this document, the default algorithm list included none. The none algorithm in JWS (JSON Web Signature, RFC 7515) means the token carries no signature whatsoever — the signature portion is an empty string.
A compliant OIDC client that naively trusts the server’s advertised algorithm list and accepts tokens signed with none can be fed a completely forged id_token. The attacker only needs to craft a valid JWT header/payload, set "alg": "none", strip the signature, and present it to the relying party.
// VULNERABLE: Default algorithm list in oidcProvider plugin initialization
// before version 1.6.11
const defaultOidcConfig = {
// ...
id_token_signing_alg_values_supported: [
"RS256",
"ES256",
"none", // ← Dangerous default: advertises unsigned token support
],
// ...
};
// A forged token accepted under this configuration:
// Header: { "alg": "none", "typ": "JWT" }
// Payload: { "sub": "admin", "iss": "https://victim.example.com", ... }
// Signature: "" (empty)
// Result: base64url(header).base64url(payload).
Any client library that performs algorithm negotiation against the discovery document — and does not independently reject none — will accept this forged token as a valid identity assertion.
Plain PKCE Instead of S256
PKCE (Proof Key for Code Exchange, RFC 7636) exists to prevent authorization code interception attacks. The S256 method derives the code challenge as BASE64URL(SHA256(code_verifier)), binding the authorization request to a secret the attacker cannot reconstruct even if they intercept the authorization code. The plain method sets the challenge equal to the verifier itself — providing zero cryptographic protection against an attacker who can observe the authorization request (e.g., via a malicious browser extension, a compromised redirect URI, or network interception in non-TLS environments).
// VULNERABLE: PKCE method defaults in the authorization endpoint handler
// before version 1.6.11
const defaultPkceConfig = {
code_challenge_methods_supported: [
"plain", // ← Accepts plain PKCE: challenge == verifier, no hashing
"S256",
],
requirePkce: false, // ← PKCE not enforced by default
};
// With plain PKCE, an intercepted authorization request reveals:
// code_challenge = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
// An attacker who intercepts this value can complete the exchange:
// code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" (identical)
The combination of both defaults in the same library amplifies risk significantly — an attacker with a network position to intercept an authorization code has everything they need to forge a session.
Impact
An attacker exploiting the none algorithm default can forge id_token values for arbitrary user identities, including administrative accounts, without possessing any signing key. This is a complete authentication bypass in deployments where the relying party accepts tokens validated against the provider’s discovery document without independently rejecting unsigned tokens.
The plain PKCE default enables authorization code interception attacks. An attacker positioned to observe the authorization request — through a shared browser environment, a malicious OAuth client, or a network interception — can redeem the authorization code themselves, obtaining valid access tokens and refresh tokens for the victim’s session.
The MCP plugin exposure is particularly relevant for AI-agent platforms, where OAuth flows are used to delegate tool access. A forged identity token in an MCP context could allow an attacker to impersonate a privileged agent or user, potentially triggering automated actions with real downstream consequences.
CVSS 8.7 HIGH reflects the low attack complexity once a suitable network position is established, the high impact to confidentiality and integrity, and the lack of required authentication for the attacker.
How to Fix It
Immediate action: upgrade to better-auth 1.6.11 or later.
# npm
npm install better-auth@latest
# pnpm
pnpm add better-auth@latest
# yarn
yarn add better-auth@latest
# bun
bun add better-auth@latest
The 1.6.11 release removes none from all advertised algorithm lists and defaults PKCE to S256-only with enforcement enabled. The corrected configuration pattern looks as follows:
// FIXED: Hardened defaults in oidcProvider plugin (>= 1.6.11)
const hardenedOidcConfig = {
id_token_signing_alg_values_supported: [
"RS256",
"ES256",
// "none" is no longer present
],
code_challenge_methods_supported: [
"S256", // Only S256 is advertised and accepted
// "plain" is no longer present
],
requirePkce: true, // PKCE is now enforced by default
};
If upgrading immediately is not possible, apply these mitigations manually in your plugin configuration:
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
oidcProvider({
// Explicitly restrict algorithms — never include "none"
allowedAlgorithms: ["RS256", "ES256"],
// Require S256 PKCE and reject plain
pkce: {
required: true,
allowedMethods: ["S256"],
},
}),
],
});
Additionally, validate at the relying-party side: any JWT consumer should independently reject tokens with "alg": "none" regardless of what the provider advertises.
Our Take
The none algorithm vulnerability is almost two decades old in the JWT ecosystem, yet it continues to appear in production libraries. The pattern is consistent: a developer implements algorithm agility for flexibility or test-mode convenience, lists none in a supported values array, and ships it without recognizing that a discovery document advertising none becomes an open invitation for token forgery.
The plain PKCE issue follows the same logic — plain exists in the specification for backward compatibility and constrained environments, not as a modern default. Defaulting to it is equivalent to defaulting to HTTP instead of HTTPS and calling it “developer-friendly.”
For enterprise development teams, this vulnerability underscores a critical design principle: security-sensitive defaults must be the most restrictive option, not the most permissive. Flexibility should require explicit opt-in. Authentication libraries that handle OAuth 2.0 and OIDC are particularly high-stakes — a weak default in an auth library propagates to every application built on top of it.
Organizations using better-auth in production should treat this as a P1 patch. Any deployment acting as an OIDC provider, especially those exposed to third-party relying parties or AI agent infrastructure via MCP, should consider all sessions established under affected versions as potentially compromised pending investigation.
Detection with SAST
This vulnerability class maps primarily to CWE-327 (Use of a Broken or Risky Cryptographic Algorithm) and CWE-1391 (Use of Weak Credentials) in the context of PKCE method selection.
Offensive360’s SAST engine flags this pattern through several rule categories:
- Algorithm whitelist enforcement: Any string literal
"none"appearing in arrays assigned to JWT algorithm configuration properties (e.g.,algorithms,allowedAlgorithms,id_token_signing_alg_values_supported) triggers a HIGH finding. - PKCE method analysis: Data flow tracking from PKCE configuration objects identifies when
"plain"is present incode_challenge_methods_supportedwithout a correspondingrequireS256: trueor equivalent constraint. - Insecure default propagation: Taint analysis traces plugin default objects through merging and spread operations to determine whether insecure values reach the final runtime configuration even when overrides are partially applied.
- Discovery document generation: Any code path that serializes a configuration object into a
.well-known/openid-configurationresponse is instrumented to verify the serialized algorithm list does not containnone.
These rules operate at the AST level in TypeScript and JavaScript codebases, meaning they catch the pattern regardless of whether the insecure value is a literal, a constant reference, or a default parameter in a function signature.
References
Detect this vulnerability class in your codebase
Offensive360 SAST scans your source code for CVE-2026-67336-class vulnerabilities and thousands of other patterns — across 60+ languages.