---
name: OAuth Hunt
slug: oauth-hunt
category: Quality
description: OAuth Hunt finds authorization and identity bugs in OAuth 2.0/2.1, OIDC, SAML SSO, and JWT flows. Use it when testing login, token, redirect, PKCE, claim, or federation logic in web apps, MCP servers, GitOps tools, and identity providers.
github: "https://github.com/H-mmer/pentest-agents/tree/main/skills/hunt-oauth"
language: Python
stars: 804
forks: 156
install: "npx degit https://github.com/H-mmer/pentest-agents/tree/main/skills/hunt-oauth ~/.claude/skills/hunt-oauth"
installs_to: ~/.claude/skills/hunt-oauth
source_path: skills/hunt-oauth/SKILL.md
collection_size: 25
category_size: 1354
collection_url: "https://dirskills.com/collections/H-mmer/pentest-agents"
added: 2026-08-22T05:22:30.902Z
last_synced: 2026-08-22T05:22:30.902Z
canonical_url: "https://dirskills.com/skills/oauth-hunt"
---

# OAuth Hunt

OAuth Hunt finds authorization and identity bugs in OAuth 2.0/2.1, OIDC, SAML SSO, and JWT flows. Use it when testing login, token, redirect, PKCE, claim, or federation logic in web apps, MCP servers, GitOps tools, and identity providers.

**Install:**

```bash
npx degit https://github.com/H-mmer/pentest-agents/tree/main/skills/hunt-oauth ~/.claude/skills/hunt-oauth
```

## README

## Crown Jewel Targets

OAuth/OIDC/SAML/JWT is the universal entry point to *every* enterprise account. A single missing `state` check or loose `redirect_uri` regex turns into a one-click ATO and the program rates it Critical because the impact is multiplicative — once you ride someone's session, you have all their data, all their integrations, all their tokens. The 24-month meta has shifted decisively toward six asset types. All CVEs below verify against NVD.

**1. MCP servers and agentic LLM OAuth (the new gold rush, 2025-2026 meta).** Model Context Protocol servers exploded in adoption with broken OAuth implementations. **CVE-2025-4143** (Cloudflare workers-oauth-provider missing redirect_uri validation, fixed v0.0.5) and **CVE-2025-4144** (PKCE downgrade in same library) define the opening salvo. Obsidian Security's Square MCP one-click ATO (July-September 2025 disclosures, fixed late September 2025) showed anonymous-cookie injection chained with IdP consent caching. **CVE-2025-6514** (mcp-remote OS command exec via crafted `authorization_endpoint` URL, 558,846 downloads affected, JFrog disclosure) and **CVE-2025-49596** (Anthropic MCP Inspector unauth RCE, 38K weekly downloads). FastMCP OAuth Proxy missing `resource` validation (GHSA-5h2m-4q8j-pqpj) means tokens issued for one MCP server work against any MCP server sharing the same authorization server. Hunt MCP servers first — every developer is wiring up OAuth for the first time and getting it wrong.

**2. SAML on enterprise SSO (parser-differential renaissance).** **CVE-2025-25291** + **CVE-2025-25292** (ruby-saml signature wrapping via REXML/Nokogiri parser differential, ahacker1 + Peter Stöckli, GHSL-2024-329 + GHSL-2024-330, GitHub Security Lab) opened a critical hole in any Ruby app using SAML — including unauthenticated admin access to GitLab Enterprise as demonstrated by Gareth Heyes and Zakhar Fedotkin in PortSwigger's "SAML Roulette" research (March 2025). **CVE-2025-46572** (passport-wsfed-saml2 SAML signature wrapping, Auth0/Okta), **CVE-2025-47949** (samlify SAML signature wrapping, npm `samlify < 2.10.0`), **CVE-2024-45409** (ruby-saml ahacker1 baseline). Pair this with Admidio SAML (GHSA-p9w9-87c8-m235 ACS URL injection, GHSA-25cw-98hg-g3cg signature validation result discarded — both 2026, high severity). Old SAML libraries on enterprise SSO are paying again.

**3. JWT algorithm confusion (the regression class).** **CVE-2026-22817** (Hono JWT middleware RS256→HS256 confusion, CVSS 8.2, Cloudflare Workers/Deno/Bun ecosystem, fixed 4.11.4), **CVE-2026-22818** (Hono JWK middleware untrusted header.alg fallback when JWK lacks `alg`, GHSA-3vhc-576x-3qv4), **CVE-2024-54150** (cjwt C library RS/EC/PS algorithm confusion, fixed 2.3.0), **CVE-2024-37568** (Authlib HMAC verification with asymmetric public key, milliesolem disclosure), **CVE-2025-61152** (python-jose alg=none, disputed but real on `verify_signature: False` configurations). Every JWT library that doesn't pin algorithms is exploitable. Greppable in 30 seconds with `jwt.verify(token, secret)` patterns.

**4. nOAuth and Entra ID identity confusion (Microsoft pays directly).** **CVE-2024-21632** (omniauth-microsoft_graph nOAuth, GHSA-5g66-628f-7cvj, fixed 2.0.0) is the canonical disclosed example of Descope's June 2023 nOAuth research — apps trusting the `email` claim from Entra ID get cross-tenant ATO. Semperis found 9% of Entra Gallery apps still vulnerable in June 2025; estimated 15,000+ SaaS apps still exposed. Descope earned $75K+ in coordinated bounties for the original nOAuth class. **CVE-2025-55241** (Dirk-jan Mollema, Entra ID actor token cross-tenant Global Admin impersonation, CVSS 10.0, fixed July 17 2025) extended the class to *Microsoft itself* — any free Entra tenant could impersonate any user in any other tenant via the legacy Azure AD Graph API not validating the `actort` token's originating tenant. Hunt every "Sign in with Microsoft" button by registering a free Entra tenant and changing your email.

**5. Authorization server implementations on managed identity platforms.** **CVE-2024-52289** (Authentik OAuth2 regex redirect_uri bypass via unescaped `.`, Lukas Omegapoint disclosure, fixed 2024.10.3 / 2024.8.5, GHSA-3q5w-6m3x-64gj), **CVE-2024-23647** (Authentik PKCE downgrade by removing `code_challenge`, fixed 2023.10.7), **CVE-2023-48228** (Authentik PKCE bypass by omitting `code_verifier`), **CVE-2024-22258** (Spring Authorization Server PKCE downgrade for Confidential Clients, fixed 1.2.3 / 1.1.6 / 1.0.6), **CVE-2026-32245** (tinyauth OIDC code not bound to client on token exchange, GHSA-xg2q-62g2-cvcm). These pay because deploying Keycloak/Authentik/Hydra/Auth0/Spring Auth Server is what every mid-size company does — find one bug, hit thousands of downstream apps.

**6. GitOps controllers and Kubernetes OIDC.** **CVE-2025-55190** (Argo CD project API token retrieves repository credentials, CVSS 7.7, GHSA-786q-9hcg-v9ff, fixed 3.1.2/3.0.14/2.14.16/2.13.9), **CVE-2026-23990** (Flux Operator Web UI impersonation bypass via empty OIDC claims, fixed 0.40.0, GHSA-4xh5-jcj2-ch8q), **CVE-2026-40161** (Tekton git resolver leaks system Git API token to user-controlled `serverURL`, CVSS 8.4, GHSA-wjxp-xrpv-xpff). These pay the highest because compromising the GitOps controller gives you cluster-wide admin and supply-chain RCE in one chain.

**7. The OAuth/SSO supply-chain — Salesloft Drift class.** UNC6395 / ShinyHunters used stolen Drift OAuth tokens to exfiltrate 1.5 billion Salesforce records from 760 companies (August 2025). The pivot was: GitHub repo compromise → AWS access → Drift OAuth refresh tokens → Salesforce + Google Workspace. Cloudflare alone had 104 API tokens leaked through Salesforce support cases. Bug bounty correlate: hunt for *third-party OAuth integrations* with overscoped tokens and no rotation policy. Programs paying for SaaS connector misconfiguration include Atlassian, Notion, Slack, Asana, Monday — all tracked under their respective HackerOne programs.

**What pays the most:** pre-auth, no user interaction, single click → ATO. Reddit one-click ATO via Sign-in-with-Apple paid Frans Rosén $10,000 (Detectify 2022, HackerOne Reddit program, disclosed at https://infosecwriteups.com/this-is-how-he-could-hijack-reddit-accounts-with-just-one-click-a-10-000-bug-bounty-7fd8d54d5582, replicated pattern still pays mid-2026 on smaller programs). Anmol Singh Yadav's race-condition OAuth token mint paid $8,500 P1 in 2025 on a Fortune 500 cloud platform — disclosed via InfoSec Write-ups https://infosecwriteups.com/how-i-hijacked-oauth-tokens-through-a-parallel-auth-flow-race-condition-8500-p1-bug-bounty-7af1cccc4d4c. Open redirect → OAuth ATO chain consistently reaches $5K-$15K range when escalating from a "Low" standalone open redirect (DEV.to lucky_lonerusher 2026 disclosed $15K bug bounty program payout https://dev.to/lucky_lonerusher/open-redirect-to-account-takeover-the-exploit-chain-most-hunters-miss-in-2026-3j1g). Vercel's WAF-bypass H1 program pays separately for bypasses against React Server Components endpoints (CVE-2025-66478 / CVE-2025-55182). On GitHub Security Lab: ruby-saml ahacker1 + Peter Stöckli engagement was a **paid private bug bounty engagement** (amount undisclosed, but classed as "blockbuster" by GitHub). Standalone "missing state parameter on /oauth/callback" pays $200-$2K depending on chainability per disclosed HackerOne corpus pattern — never submit it alone.

## Attack Surface Signals

Greppable / fingerprintable / scannable. Every signal below is tied to a specific product class and the resulting CVE candidate.

**HTTP-level signals on a live target**:

- `Set-Cookie: oauth2_state=`, `oauth_state=`, `__Host-state=` → custom OAuth client → **state CSRF candidate**, test removal/replay
- `redirect_uri=https%3A%2F%2F` in any 302 Location response → **redirect_uri validation candidate** (test substring/path/userinfo bypasses)
- `?state=`, `?code=`, `&code=`, `#access_token=`, `#id_token=` in URL → **OAuth callback page** — audit for third-party JS leakage (dirty dancing)
- `WWW-Authenticate: Bearer realm="MCP"` or `Bearer realm="..."` → **OAuth Resource Server / MCP server** — fingerprint via RFC 9728 metadata at `/.well-known/oauth-protected-resource`
- `WWW-Authenticate: ... resource_metadata="..."` → **MCP 2025-11-25 spec compliant server** — discover OAuth flow; test PKCE downgrade and audience confusion
- `Server: nginx-openid-connect`, `nginx-auth-request`, `oauth2-proxy/` → **CVE-2025-54576 oauth2-proxy `skip_auth_routes` query param bypass** candidate; **CVE-2024-10318 NGINX OIDC nonce session fixation** on the `nginx-openid-connect` family
- `X-Forwarded-User`, `X-Auth-Request-User`, `X-Auth-Request-Email`, `X-Auth-Request-Groups` → reverse-proxy auth (oauth2-proxy / Pomerium / authelia / Authentik forward-auth) — try header injection bypass on backend
- 500/400 errors with `goauthentik`, `Authentik`, `pyAuth` in body → **Authentik** — CVE-2024-52289 / CVE-2024-23647 / CVE-2023-48228 candidate
- `X-Powered-By: Express`, `Set-Cookie: connect.sid` plus `/oauth2/authorize` → Node OAuth provider — Hono / Passport.js JWT confusion candidate (CVE-2026-22817 / CVE-2025-46572)
- 302 from `/login` / `/sso` to `/saml/sso/`, `/saml2/idp/SSOService`, `/idp/profile/SAML2/Redirect/SSO`, `/Shibboleth.sso/` → **SAML SP** — try ruby-saml CVE-2025-25291/25292 parser differential payload
- `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)
- `<script src="https://*.fullstory.com/`, `*.hotjar.com/`, `*.intercom.io/`, `*.drift.com/`, `*.salesloft.com/` on OAuth callback → **session-replay tool URL exfil** + supply-chain risk (Salesloft Drift August 2025)
- `react-server-dom-webpack`, `react-server-dom-parcel`, `react-server-dom-turbopack` in `package.json` or bundles + OAuth callback → **CVE-2025-66478 / CVE-2025-55182 React2Shell on the OAuth callback handler** (RCE on backend)
- `localStorage.getItem("access_token")`, `localStorage.setItem("oauth_token"`, `sessionStorage["jwt"]` in JS → **token in webstorage** — XSS becomes ATO

**Source-code signals** (ripgrep one-liners — see Source Review for the full set):

```bash
# 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_token` to a code-flow auth URL → Detectify dirty-dancing fragment leak.
- **URL fragment** — `#access_token=`, `#id_token=`, `#code=` from implicit flow or `response_mode=fragment`. JS on the callback page reads `location.hash`; if any third-party script also reads it → leak.
- **Headers** — `Authorization: Bearer`, `Authorization: DPoP`, `X-Forwarded-User`, `X-Auth-Request-Email`, custom OIDC headers from reverse proxy. Try header injection at the backend after a reverse-proxy strips them. Also `Origin:` for CORS bypass on OAuth endpoints (Grab partner-api H1 #3631550 — null Origin reflected with credentials).
- **Body (form-urlencoded)** — `/oauth/token` body parameters: `grant_type`, `code`, `code_verifier`, `client_id`, `client_secret`, `refresh_token`, `redirect_uri`, `subject_token`, `subject_token_type`, `actor_token` (RFC 8693 token exchange). PKCE downgrade → drop `code_verifier`. Race condition → Turbo Intruder 2 parallel exchanges with same `code`.
- **Body (JSON)** — Dynamic Client Registration body: `client_name`, `redirect_uris`, `grant_types`, `token_endpoint_auth_method`, `jwks`, `jwks_uri`, `software_statement`. SSRF via `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

1. **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.

2. **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).**

3. **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).**

4. **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 th
