PKCE Downgrade Attack: When the Authorization Server Accepts Both Flows

Two CVEs against the same identity provider, filed six months apart. CVE-2023-48228 and CVE-2024-23647, both against authentik, describe the same failure: the server accepted PKCE in one request and its absence in the next. RFC 7636 had been published nine years before the first advisory. PKCE binds an authorization code to a secret the client generates before the flow starts. Authorization servers that accept flows without code_challenge downgrade clients to the pre-PKCE threat model. The same applies when they accept token exchanges without code_verifier after a challenge was registered. RFC 9700 mandates enforcement at both endpoints. Most servers ship with enforcement disabled. PKCE Binds the Code to a Verifier the Server Never Sees in the Authorization Request The security model of PKCE depends on one fact. The code_verifier is never transmitted in the authorization request, so intercepting that request does not yield the secret needed to exchange the code. RFC 7636 §4.1 requires the client to generate a code_verifier of 43 to 128 characters of high-entropy cryptographic random data. RFC 7636 §4.2 defines code_challenge = BASE64URL(SHA256(code_verifier)), with S256 required for all conforming servers. The client sends code_challenge in the authorization request; the server stores the challenge alongside the issued code. In the token exchange, the client sends code_verifier. The server computes SHA256(verifier) and compares it to the stored challenge; a mismatch returns invalid_grant. The server sees the challenge in step 1 and the verifier in step 2, never both at the same time. The Downgrade Path: Strip code_challenge Before It Reaches the Server A server that treats code_challenge as optional validates the verifier only when the parameter is present. An attacker who removes the parameter before the request arrives gets an unbound authorization code, exchangeable without a verifier. The attack has five steps. The victim initiates an OAuth flow from a public client: a SPA or mobile app. The attacker intercepts the outbound authorization request via HTTP MITM, browser extension, or malicious redirect handler. The attacker removes code_challenge and code_challenge_method from the request. The server, without enforcement, accepts a standard non-PKCE request and issues a code with no verifier binding. The attacker captures the code via redirect and exchanges it at the token endpoint without any code_verifier. CVE-2023-48228 (authentik < 2023.10.4 and < 2023.8.5, CVSS 7.5): the token endpoint validated code_verifier only when provided. Omitting it in the token exchange succeeded even when the flow had started with code_challenge. Network vector, no privileges required. CVE-2024-23647 (authentik <= 2023.10.6 and <= 2023.8.6, CVSS 6.5): the authorization endpoint did not reject requests missing code_challenge. An attacker could deliver a phishing link with the flow already stripped of PKCE; the server issued an unbound code without error. Same codebase, six months after the first CVE. CVE-2026-48717 (OpenAM through 16.0.6, CVSS 6.5): enforcement of code_verifier was gated on a realm-wide setting called codeVerifierEnforced, disabled by default. Every new installation accepted token exchanges without a verifier. Public Clients Cannot Fall Back: PKCE Is Their Only Code Binding Confidential clients authenticate at the token endpoint with a client secret. SPAs and native apps have no secret to present at that endpoint. PKCE is the only mechanism that binds the authorization code to the client that started the flow. RFC 7636 §1 describes the original threat model: the OS allows multiple apps to register the same custom URI scheme. A malicious app on the same device intercepts the redirect before the legitimate app handles it, capturing the code. Without PKCE, the code is redeemable by any recipient. In SPAs, the authorization code arrives in the redirect URL. Referer headers propagate that URL to analytics scripts, CDN, and ad networks loaded immediately after the redirect. With a server that does not enforce PKCE, stripping the PKCE parameters removes the protection before the request reaches the AS. Five CVEs, Four Codebases, Same Root Cause RFC 9700 §2.1.1 sets three MUST-level requirements. Servers MUST support PKCE and MUST enforce the correct code_verifier at the token endpoint when a code_challenge was in the authorization request. They MUST also mitigate PKCE downgrade attacks. RFC 9700 §4.8.2 adds the reverse: the server MUST reject token requests containing code_verifier when no code_challenge was in the authorization request. Five CVEs from 2023 to 2026, across four independent codebases, share the same pattern: enforcement conditional on parameter presence, not mandatory. CVE-2023-48228 (authentik, CVSS 7.5): token endpoint validated the verifier only when provided. CVE-2024-23647 (authentik, CVSS 6.5): authorization endpoint did not reject missing code_challenge; same codebase, six months later. CVE-2026-48717 (OpenAM, CVSS 6.5): enforcement gated on a realm-wide setting that ships disabled. CVE-2026-41213 (@node-oauth/oauth2-server): the server accepted code_verifier shorter than 43 characters, the RFC 7636 §4.1 minimum; short verifiers are brute-forceable. CVE-2026-67336 (better-auth): the plain method was accepted by default. With plain, code_verifier and code_challenge are identical in cleartext; any interceptor of the authorization request captures the verifier without computation. None of these are cryptographic errors: all are enforcement logic failures. Detection Requires Testing Both Enforcement Points Verifying that a server meets RFC 9700 requires three independent tests. The presence of code_challenge_methods_supported: S256 in AS discovery metadata confirms the server can validate PKCE, not that flows without code_challenge are rejected. First test: send an authorization request for a public client without code_challenge. An RFC 9700-compliant server must reject it; acceptance confirms downgrade is possible. Second test: complete a normal PKCE authorization, then exchange the code without code_verifier. The server must return invalid_grant. This is the exact failure mode of CVE-2023-48228: a successful token response indicates the token endpoint does not enforce. Third test: exchange a non-PKCE code with code_verifier included in the request. RFC 9700 §4.8.2 requires rejection. Failure here enables the verifier injection variant, where the attacker inserts a verifier into a non-PKCE exchange to confuse the server's validation logic. Most automated scanners test only one of these three points; all three must pass. The Fix Is Server-Side Enforcement at the Authorization Request Telling client developers to use PKCE does not close the downgrade gap if the server continues accepting authorization requests without code_challenge. Enforcement must happen at the server, where the request arrives. In Keycloak, "PKCE Code Challenge Method" per client must be set to S256, not "none". With S256, the server rejects authorization requests without code_challenge for that client. In Spring Authorization Server, RegisteredClient.Builder.requireProofKey(true) produces the same effect. For authentik, upgrading to 2023.10.7 or later (or 2023.8.7 in the 2023.8.x branch) fixes both CVEs described above. The MAGO Intel tool (intel.mago.team) tests all three enforcement points during OAuth security assessments and reports which RFC 9700 §2.1.1 or §4.8.2 requirement the server fails. RFC 9700 is eight years after RFC 7636. Servers that shipped PKCE support without PKCE enforcement have had every version since then to add the second half. The code_challenge_methods_supported field in AS metadata confirms the server can validate PKCE, not that it does. The test that matters is sending a token exchange without code_verifier after a PKCE-initiated flow and confirming the server returns invalid_grant. If it returns a token instead, the client's PKCE protection does nothing.
Take Your Experience to the Next Level
NewDownload our mobile app for a faster and better experience.
Comments
0U
Join the discussion
Sign in to leave a comment