oauth-flow-hijack · git:20260504.2a64374 · 2026-05-04 · sha256 53b0faa106fbaada

oauth-flow-hijack git:20260504.2a64374A

Immutable. This exact content is served forever at /api/v1/blob/53b0faa106fbaada.

---
name: oauth-flow-hijack
description: Techniques to hijack OAuth flows by stopping redirects, leaking authorization codes, and exploiting popup or iframe name collisions. Use when testing OAuth implementations, especially when you have XSS on a subdomain, an open redirect, or a way to prevent callback consumption.
---

# OAuth Flow Hijack Techniques

## 1. Stopping Client-Side Redirects

### Navigation rate limiting
Some browser versions throttle excessive navigations. If the target relies on client-side redirects, throttling can leave the callback page rendered with the code still present in the URL.

### Sandboxed frame without `allow-forms`
Load the target in a sandbox that permits scripts but blocks form submission. This can break automatic callback completion logic while still allowing attacker-controlled JavaScript contexts nearby.

### Dangling markup in URL
Certain browser behaviors treat malformed client-side navigation targets as blocked. If part of the redirect URL is attacker-influenced, malformed fragments can stop the navigation.

### URL overflow
Large `state` values can trigger 414, 431, or 500 responses before the authorization code is consumed.

## 2. Leaking Authorization Codes

### Navigation API history leak
After forcing an error state, redirect to a readable origin such as `about:blank` and inspect navigation history where available.

### Cookie bombing -> 431
Oversized cookies scoped to the callback path can force a 431 response, leaving the code unconsumed.

### WAF-triggered callback failure
If a WAF blocks callback parameters, the code can remain valid because the application never processes it.

### Analytics endpoint abuse
In some CSP-constrained cases, attacker-controlled outbound POST destinations can still receive sensitive callback artifacts through trusted telemetry endpoints.

## 3. Popup or Iframe Name Hijack

If the application uses a predictable `window.open(url, "fixedName")` target:
1. Pre-create a same-origin frame or popup with that name.
2. Wait for the victim to trigger the OAuth flow.
3. Let the application reuse the attacker-controlled browsing context.
4. Redirect that context to an attacker-chosen callback or observe the resulting messages.

## Detection Checklist
- Static or predictable `window.open()` target names
- OAuth callback pages posting to `window.opener`
- Missing or inconsistent `frame-ancestors` protection on same-origin assets
- Weak `state` validation
- Large callback parameters accepted without strict bounds

## 4. PKCE Downgrade Attack

OAuth 2.1 mandates PKCE. Many servers added PKCE *support* without adding PKCE *enforcement* — they accept `code_challenge` when present but don't reject requests missing it.

### Test: Strip PKCE from Authorization Request

```
# Normal 2.1-compliant request
GET /authorize?response_type=code&client_id=CLIENT&redirect_uri=CALLBACK&code_challenge=CHALLENGE&code_challenge_method=S256&state=STATE

# Downgrade: remove code_challenge and code_challenge_method
GET /authorize?response_type=code&client_id=CLIENT&redirect_uri=CALLBACK&state=STATE
```

If the server issues an authorization code without `code_challenge`, PKCE is decorative. The code can be exchanged without `code_verifier` — any interceptor (open redirect, Referer leak, proxy log) gets a usable code.

### CVE-2025-4144: Cloudflare Workers PKCE Bypass (Reverse Direction)

The `handleTokenRequest` function accepted `code_verifier` even when the authorization request omitted `code_challenge`. Attack: initiate OAuth without PKCE, steal the code (via Referer/open redirect/log), then exchange it with any arbitrary `code_verifier` — the server doesn't check because no challenge was stored.

```bash
# Step 1: authorize without PKCE (or steal code from a non-PKCE flow)
# Step 2: exchange with fabricated verifier
curl -X POST https://target.com/token \
  -d "grant_type=authorization_code&code=STOLEN_CODE&redirect_uri=CALLBACK&client_id=CLIENT&code_verifier=anything"
```

If token is issued, PKCE enforcement is broken.

### Priority Targets for PKCE Downgrade

- Mobile apps with legacy non-PKCE codepaths still active
- Servers claiming OAuth 2.1 compliance (check `/.well-known/openid-configuration` for `code_challenge_methods_supported`)
- Hybrid deployments with 2.0 and 2.1 endpoints coexisting on same authorization server

## 5. Framework-Specific OAuth Bypasses

Fingerprint the OAuth stack first (error pages, headers, JS bundles, `/.well-known/openid-configuration`), then test framework-specific attack patterns.

### django-allauth Mutable Claim Takeover (< 65.13.0)

django-allauth uses `preferred_username` (OIDC claim) as the account UID for certain IdPs (Okta, NetIQ). This claim is mutable by the user on the IdP side.

**Attack:**
1. Identify target uses django-allauth + Okta/NetIQ (check `/accounts/login/`, error pages, `allauth` in JS/HTML)
2. Create account on the IdP
3. Change your `preferred_username` to the victim's username
4. Log in to the target — django-allauth matches your mutable claim to the victim's account

**Detection:** Any app using `preferred_username` or `email` as the sole identity anchor without an immutable IdP sub claim is vulnerable.

### CVE-2025-54576: OAuth2-Proxy skip_auth_routes Regex Bypass

`skip_auth_routes` regex is evaluated against the entire request URI including query parameters, not just the path.

```
# Server config
skip_auth_routes = [ "^/public/.*$" ]

# Bypass: match the regex via query string
GET /admin/secret?x=/public/anything HTTP/1.1
# -> regex matches -> authentication skipped
```

**Detection:** Identify oauth2-proxy via `Gap-Auth` response header or `_oauth2_proxy` cookie names.

## Detection Checklist (Extended)
- Static or predictable `window.open()` target names
- OAuth callback pages posting to `window.opener`
- Missing or inconsistent `frame-ancestors` protection on same-origin assets
- Weak `state` validation
- Large callback parameters accepted without strict bounds
- PKCE parameters optional on `/authorize` (strip and test)
- `code_verifier` accepted without prior `code_challenge` on `/token`
- `preferred_username` or `email` used as identity anchor (mutable claim risk)
- `oauth2-proxy` or `Gap-Auth` header present (regex bypass candidate)
- `/.well-known/openid-configuration` exposes `registration_endpoint` (DCR — see `mcp-auth-exploitation` skill)

## Chain With
- `dom-vulnerability-detection`
- `web-cache-deception-path`
- `race-condition-single-packet`
- `mcp-auth-exploitation` (DCR/CIMD SSRF when registration_endpoint is exposed)
- `auth-matrix-testing` (JWT algorithm confusion on OAuth-issued tokens)

## References
- https://lab.ctbb.show/research/stopping-redirects
- https://lab.ctbb.show/research/can-a-predicted-window-open-target-really-be-that-impactful
- https://lab.ctbb.show/writeups/bypassing-csp-new-relic-custom-events-cspt
- https://labs.detectify.com/writeups/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/
- https://blog.criticalthinkingpodcast.io/p/hackernotes-ep-169-oauth-2-1-mcp-authorization-security (PKCE downgrade, framework CVEs)