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.
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:
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 onThe 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.
stateis CSRF protection for the callback. It is an unguessable value the client mints, parks in the session, and verifies on return. Nostate, or astatethe 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). Missingstateis its own bug, independent of redirect handling.- PKCE binds the authorization code to the client instance that started the
flow, via
code_verifierandcode_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:
- Show the response that places
code(or a token, in implicit flow) in a URL sent to your controlled destination. Capture the actual value. - 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.
- 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.