3 min readOAUTH · AUTHENTICATION · WEB

OAuth redirect_uri allowlists and the state you forgot

Redirect_uri allowlist bypasses, the real job of state and PKCE, and why most "open redirect in OAuth" reports get downgraded unless you show token theft.

user authorize ?REDIRECT_URI allowlist PREFIX MATCH callback ON ALLOWLIST MATCH trusted.com.evil CRAFTED REDIRECT_URI token to attacker CODE EXFIL SLIPS CHECK TOKEN
A crafted redirect_uri that slips the allowlist and steals the token

OAuth gives an attacker one thing to aim at: the redirect_uri. If you can make the authorization server send the code or token to a URL you control, the rest of the flow hands it to you. Everything below is about beating the allowlist that is supposed to stop that, and about proving the impact instead of the redirect.

Test the matcher, not the happy path

The allowlist is a string comparison, and string comparisons are where this lives. Start from the registered value and probe each assumption the matcher might be making. Given a legitimate https://app.example.com/callback, I walk these in order:

plaintext
https://app.example.com.attacker.com/callback   suffix not anchored
https://app.example.com@attacker.com/callback    userinfo confusion
https://attacker.com/app.example.com/callback     substring match
https://app.example.com/callback/../../evil       path normalisation
https://app.example.com/callback/.attacker.com    appended segment
https://app.example.com.attacker.com              host prefix match only
http://app.example.com/callback                   scheme downgrade
https://APP.EXAMPLE.COM/callback                   case handling
https://app.example.com:1337/callback              port not pinned
https://sub.app.example.com/callback               subdomain wildcard
http://localhost:1337/callback                     dev allowance left on

The open-redirect-on-an-allowlisted-host chain is the one people miss. The authorization server is happy because redirect_uri is genuinely on the allowlist; the allowlisted endpoint then bounces the browser, code in the URL, to your domain. Two boring bugs, one critical.

What state and PKCE are actually for

These get conflated, and conflating them costs you findings.

  • state is CSRF protection for the callback. It is an unguessable value the client mints, parks in the session, and verifies on return. No state, or a state the server never checks, means an attacker can fixate a login: complete a flow with their own code, splice it onto the victim’s session, and the victim is now logged into the attacker’s account (or the reverse, depending on the flow). Missing state is its own bug, independent of redirect handling.
  • PKCE binds the authorization code to the client instance that started the flow, via code_verifier and code_challenge. It defends a code that leaks in transit (interception, a redirect bug, a referer). Without PKCE, a stolen code is redeemable by anyone; with it, the thief also needs the verifier.

The two are complementary. state stops the swap at the callback, PKCE stops a leaked code from being spent. A flow missing both is materially weaker than one missing either.

Why the redirect alone gets downgraded

Here is the part that decides whether a report lands. An “open redirect in the OAuth flow” with no further work reads as a low-severity redirect, and triage will treat it as one. The severity is not the redirect, it is what rides the redirect.

To make it a real finding, walk the credential out:

  1. Show the response that places code (or a token, in implicit flow) in a URL sent to your controlled destination. Capture the actual value.
  2. If PKCE is absent or the client accepts a public verifier, redeem that code at the token endpoint and show the access token coming back.
  3. If you cannot redeem it directly, show the code landing in your logs via referer or the redirect, which is still account takeover against any user you can get to start a flow.

The difference between “open redirect” and “full account takeover via OAuth” is one paragraph proving the code reached you and was spendable. Without that paragraph the report is a footnote. With it, it is the headline. Always do the redemption step, or show exactly why the leaked code is sufficient on its own.