SECURE_NODE // ATHENS_GR
← BACK TO RESEARCH Technique · Auth

Bypassing PKCE

28 MAY 2026 · OAuth / PKCE / OIDC

Methodology write-up. No client systems or live findings are described here. The flow below is reproduced against a throwaway lab app.

FINAL SEVERITY
High
account takeover
ATTACK COMPLEXITY
Low
no secret needed
KEY FLAW
PKCE
verifier not enforced

PKCE (Proof Key for Code Exchange) exists to stop authorization-code interception. The flow is elegant on paper. In practice, the protection is only as strong as the verification step — and that is where things go wrong.

The thirty-second refresher

The client generates a random code_verifier, hashes it into a code_challenge, and sends the challenge up front. Later, when exchanging the code for a token, it sends the original verifier. The server hashes the verifier and checks it matches the challenge it stored. No match, no token.

Where the protection silently disappears

The mechanism only protects you if every link in the chain is enforced:

  • Challenge not bound to the code. If the server does not tie the stored challenge to the specific authorization code, an attacker can mix and match.
  • plain method accepted. Some servers still allow code_challenge_method=plain, which makes the challenge equal to the verifier — defeating the whole point.
  • Verifier check skipped on certain paths. Alternate token endpoints or grant types sometimes never re-run the comparison.

The exchange that should fail

Here is the token request I replay with a verifier that does not match the captured challenge. On a correct implementation it returns invalid_grant. On a broken one, it hands over a token:

POST /oauth/token HTTP/1.1
Host: auth.acme-lab.test
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<captured_auth_code>
&code_verifier=deliberately_wrong_value
&redirect_uri=https://app.acme-lab.test/callback
&client_id=public-spa-client

If a 200 OK with an access_token comes back, PKCE is present in the request but absent in the enforcement.

The test timeline

T+0:00 — CAPTURE

Captured a normal flow. Confirmed the code_challenge / code_verifier pair and the S256 method on the authorize call.

T+0:04 — DOWNGRADE

Tried the plain downgrade. Re-sent the authorize request forcing code_challenge_method=plain — the server accepted it.

T+0:09 — REPLAY

Replayed a code with a mismatched verifier. Token endpoint returned 200 OK with a valid access_token. Verifier never checked.

T+0:15 — REUSE

Re-redeemed the same code twice. Second exchange also succeeded — codes were not single-use.

T+0:22 — REPORT

Documented the chain. Request/response pairs, both downgrade and mismatch paths, and the code-reuse evidence attached to the finding.

The takeaway

PKCE is not a checkbox. “We implemented PKCE” and “PKCE actually protects us” are two different claims, and the gap between them is where the engagement value sits.