Quick answer
OAuth adds attack surface: each step (authorize, callback, token exchange) has its own class of bug. The four most exploited vulns: missing state parameter (CSRF in account linking → ATO), poorly validated redirect_uri (token leak), response_type confusion (code/token sent to the wrong domain), and storing tokens in the URL (leaked via Referer/cache). Typical bounties: €1,000-€8,000 depending on severity.
The OAuth flow (refresher)
[Cliente] [Authorization Server] [Resource Server]
| | |
| /authorize?client_id=... | |
| &redirect_uri=... | |
| &state=... | |
| &response_type=code | |
| ─────────────────────────> | |
| | |
| 302 redirect_uri?code=... | |
| &state=... | |
| <───────────────────────── | |
| | |
| POST /token | |
| code + client_secret | |
| ─────────────────────────> | |
| | |
| access_token + refresh | |
| <───────────────────────── | |
| | |
| GET /api (Bearer token) |
| ────────────────────────────────────────────────────────> |
Each arrow has a possible point of failure.
1. State parameter — OAuth's CSRF mitigation
state is a unique random value the client generates and verifies on the way back. Without state:
- The attacker starts an OAuth flow with their IdP account.
- Captures their own
codein the callback. - Makes the victim visit
https://target.tld/oauth/callback?code=<atacante_code>. - The target exchanges the code → receives the attacker's access_token → links the attacker's account to the victim's session.
Result: the victim is now "logged in as the attacker" in their target account → the attacker also has access, and can see what the victim does, or if the flow is "link account", the victim has linked their account to the attacker's (ATO if the attacker can later log in via the IdP).
Bypasses when state exists but is poorly done
- Predictable state (timestamp, IP hash). If you can reproduce it, it doesn't protect.
- State not validated server-side. If the client sends it but the server only verifies it's present (not that it matches the originally generated one), bypass.
- State omitted and accepted. Remove it from the callback URL → does the server process it anyway?
2. redirect_uri bypass — the most common
The redirect_uri must be registered/validated by the authorization server. If validation is lax, the code/token is redirected to the attacker's domain.
Typical bypasses
# Original registrada: https://app.target.tld/callback
# Subdomain takeover en parent domain
https://app.target.tld.attacker.tld/callback
# Path traversal
https://app.target.tld/callback/../redirect/external?url=attacker.tld
# Open redirect en endpoint del client
https://app.target.tld/redirect?url=attacker.tld (si /redirect existe y redirige)
# Backslash normalization (ya documentado en otro artículo)
https://app.target.tld/\attacker.tld
# URL parsing differential
https://app.target.tld@attacker.tld/callback
https://app.target.tld#@attacker.tld/callback
https://app.target.tld%23.attacker.tld/callback
When the authorization server does partial validation
If the AS validates only the domain (not the full path) and the client has an endpoint that reflects part of the query string, you can inject HTML/JS that leaks the code via Referer or window.location.
3. Response type confusion
OAuth supports several response_type values:
code— Authorization Code flow (standard).token— Implicit flow (deprecated, returns the access_token directly in the URL fragment).code id_token— OpenID Connect hybrid.none,id_token, etc.
The bug
Some clients register only the code flow but the AS accepts a downgrade to token:
https://as.target.tld/authorize?
client_id=app
&redirect_uri=https://attacker.tld/cb (si redirect_uri es laxo)
&response_type=token (downgrade a implicit)
&scope=read
The AS responds with https://attacker.tld/cb#access_token=...&token_type=bearer. The access_token goes in the URL fragment → Referer and proxies can capture it.
4. Code/Token leakage
Via Referer header
<!-- Página atacante en https://attacker.tld/leak -->
<img src="https://target.tld/?key=..." />
If the post-OAuth callback page contains resources loaded from external domains (analytics, ads, fonts, images), the Referer header carries the full URL with the code query param.
Mitigation: callback page with <meta name="referrer" content="no-referrer"> or Referrer-Policy: strict-origin.
Via window.opener
If the OAuth flow opens a popup and the popup loads the callback with code in the URL:
// Página atacante (popup opener)
window.open("https://target.tld/oauth/start", "popup");
setInterval(() => {
try {
console.log(popup.location.href); // ¿accesible cross-origin?
} catch {}
}, 100);
If the callback page doesn't use Cross-Origin-Opener-Policy: same-origin, the opener can read part of the popup's URL.
Via postMessage
If the callback does window.opener.postMessage(window.location.search, '*') with targetOrigin *, any opener captures the code.
5. Account linking takeover
When a user links an OAuth account (Google, GitHub, Facebook) to their target account:
- The attacker creates an account on the target with email
victima@email.tld. - The attacker starts the "link Google account" flow from their session.
- Captures the flow.
- Makes the victim visit the callback with the attacker-controlled
code. - The attacker's Google account now links to the attacker-controlled target account.
- If the target allows login only with Google, the attacker can now log in using their Google → gets into the target account with email
victima@email.tld.
Mitigation: verify the email match between the OAuth provider and the target account before linking.
6. Pre-account takeover via OAuth
Attacker:
- Registers an account on the target with
victima@email.tldwithout verifying the email. - The victim later registers on the target via "Sign in with Google" using the same email.
- If the target does an automatic merge without verifying that the attacker had confirmed the email, both share the account — the attacker already has internal credentials + the victim thinks it's theirs.
Pattern: scope inflation
The client originally requests scope=read profile. If in the flow you can inject &scope=read profile admin and the AS doesn't validate strictly:
- The AS asks the user for consent with the expanded scope.
- If the user accepts without reading (common), the issued tokens have the expanded scope.
Verify that the AS rejects scopes not registered for the client_id.
Hunting checklist
- Is there a
stateparameter? Is it validated server-side? Is it predictable? - Is
redirect_urivalidated with a lax regex or startsWith? Try bypasses. - Does the client expose redirect endpoints with
?url=that serve as a pivot? - Is
response_type=tokenenabled but not normally used? - Does the callback page have
Referrer-PolicyandCOOPheaders? - postMessage with
*in the callback? - Does account linking validate the email match?
- Is
scopevalidated against the registered client_id? - Are there
/oauth/init,/oauth/start,/oauth/connectendpoints with different flows? Each potentially with its own vuln.
Related labs
Practice state CSRF, redirect_uri bypass and account linking takeover in real OAuth flows: OAuth labs.
Practice this in a lab
Oauth
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
CSRF (Cross-Site Request Forgery) — fully explained with bypasses
CSRF: how it's exploited, common defenses (tokens, SameSite, Origin), bypasses (method change, JSON, double-submit, content-type) and where to hunt it in any app.
OAuth attacks — state CSRF, redirect_uri bypass, token reuse, token IDOR
Vulnerabilities in OAuth 2.0 flows: missing state parameter, redirect_uri loose validation, cross-app token reuse, IDOR in token refresh endpoints.
Cookie flags — Secure, HttpOnly, SameSite and why they matter
HttpOnly blocks XSS-to-cookie, Secure forces HTTPS, SameSite kills CSRF. How they break and what to report when they're missing on sensitive cookies.