oauth-to-account-takeover.md (46298B)
1 --- 2 title: "OAuth to Account Takeover" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/oauth-to-account-takeover.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/oauth-to-account-takeover.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # OAuth to Account Takeover 14 15 ## Basic Information <a href="#d4a8" id="d4a8"></a> 16 17 OAuth has several versions and grant types; [oauth.net provides a concise OAuth 2.0 overview](https://oauth.net/2/) and a focused guide to the [authorization-code grant](https://oauth.net/2/grant-types/authorization-code/). This page focuses on that widely used grant, an **authorization framework that lets an application access or perform approved actions on a user's resources managed by another service**.<sup>[[24]](#references)</sup> 18 19 Consider a hypothetical website _**https://example.com**_, designed to **showcase all your social media posts**, including private ones. To achieve this, OAuth 2.0 is employed. _https://example.com_ will request your permission to **access your social media posts**. Consequently, a consent screen will appear on _https://socialmedia.com_, outlining the **permissions being requested and the developer making the request**. Upon your authorization, _https://example.com_ gains the ability to **access your posts on your behalf**. 20 21 It's essential to grasp the following components within the OAuth 2.0 framework: 22 23 - **resource owner**: You, as the **user/entity**, authorize access to your resource, like your social media account posts. 24 - **resource server**: The **server managing authenticated requests** after the application has secured an `access token` on behalf of the `resource owner`, e.g., **https://socialmedia.com**. 25 - **client application**: The **application seeking authorization** from the `resource owner`, such as **https://example.com**. 26 - **authorization server**: The **server that issues `access tokens`** to the `client application` following the successful authentication of the `resource owner` and securing authorization, e.g., **https://socialmedia.com**. 27 - **client_id**: A public, unique identifier for the application. 28 - **client_secret:** A confidential key, known solely to the application and the authorization server, used for generating `access_tokens`. 29 - **response_type**: A value specifying **the type of token requested**, like `code`. 30 - **scope**: The **level of access** the `client application` is requesting from the `resource owner`. 31 - **redirect_uri**: The **URL to which the user is redirected after authorization**. This typically must align with the pre-registered redirect URL. 32 - **state**: A parameter to **maintain data across the user's redirection to and from the authorization server**. Its uniqueness is critical for serving as a **CSRF protection mechanism**. 33 - **grant_type**: A parameter indicating **the grant type and the type of token to be returned**. 34 - **code**: The authorization code from the `authorization server`, used in tandem with `client_id` and `client_secret` by the client application to acquire an `access_token`. 35 - **access_token**: The **token that the client application uses for API requests** on behalf of the `resource owner`. 36 - **refresh_token**: Enables the application to **obtain a new `access_token` without re-prompting the user**. 37 38 ### Flow 39 40 The **actual OAuth flow** proceeds as follows: 41 42 1. You navigate to [https://example.com](https://example.com) and select the “Integrate with Social Media” button. 43 2. The site then sends a request to [https://socialmedia.com](https://socialmedia.com) asking for your authorization to let https://example.com’s application access your posts. The request is structured as: 44 45 ```text 46 https://socialmedia.com/auth 47 ?response_type=code 48 &client_id=example_clientId 49 &redirect_uri=https%3A%2F%2Fexample.com%2Fcallback 50 &scope=readPosts 51 &state=randomString123 52 ``` 53 54 3. You are then presented with a consent page. 55 4. Following your approval, Social Media sends a response to the `redirect_uri` with the `code` and `state` parameters: 56 57 ```text 58 https://example.com?code=uniqueCode123&state=randomString123 59 ``` 60 61 5. https://example.com utilizes this `code`, together with its `client_id` and `client_secret`, to make a server-side request to obtain an `access_token` on your behalf, enabling access to the permissions you consented to: 62 63 ```text 64 POST /oauth/access_token 65 Host: socialmedia.com 66 ...{"client_id": "example_clientId", "client_secret": "example_clientSecret", "code": "uniqueCode123", "grant_type": "authorization_code"} 67 ``` 68 69 6. Finally, `https://example.com` uses the `access_token` to call the Social Media resource server and access the posts covered by the approved scope.<sup>[[24]](#references)</sup> 70 71 ## Vulnerabilities <a href="#id-323a" id="id-323a"></a> 72 73 ### Open redirect_uri <a href="#cc36" id="cc36"></a> 74 75 Per [RFC 6749 §3.1.2](https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2), the authorization server must redirect the browser only to **pre-registered, exact redirect URIs**. Any weakness here lets an attacker send a victim through a malicious authorization URL so that the IdP delivers the victim’s `code` (and `state`) straight to an attacker endpoint, who can then redeem it and harvest tokens.<sup>[[5]](#references)</sup> 76 77 Typical attack workflow: 78 79 1. Craft `https://idp.example/auth?...&redirect_uri=https://attacker.tld/callback` and send it to the victim. 80 2. The victim authenticates and approves the scopes. 81 3. The IdP redirects to `attacker.tld/callback?code=<victim-code>&state=...` where the attacker logs the request and immediately exchanges the code. 82 83 Common validation bugs to probe: 84 85 - **No validation** – any absolute URL is accepted, resulting in instant code theft. 86 - **Weak substring/regex checks on the host** – bypass with lookalikes such as `evilmatch.com`, `match.com.evil.com`, `match.com.mx`, `matchAmatch.com`, `evil.com#match.com`, or `match.com@evil.com`. 87 - **IDN homograph mismatches** – validation happens on the punycode form (`xn--`), but the browser redirects to the Unicode domain controlled by the attacker. 88 - **Arbitrary paths on an allowed host** – pointing `redirect_uri` to `/openredirect?next=https://attacker.tld` or any XSS/user-content endpoint leaks the code either through chained redirects, Referer headers, or injected JavaScript. 89 - **Directory constraints without normalization** – patterns like `/oauth/*` can be bypassed with `/oauth/../anything`. 90 - **Wildcard subdomains** – accepting `*.example.com` means any takeover (dangling DNS, S3 bucket, etc.) immediately yields a valid callback. 91 - **Non-HTTPS callbacks** – letting `http://` URIs through gives network attackers (Wi-Fi, corporate proxy) the opportunity to snatch the code in transit. 92 93 Also review auxiliary redirect-style parameters (`client_uri`, `policy_uri`, `tos_uri`, `initiate_login_uri`, etc.) and the OpenID discovery document (`/.well-known/openid-configuration`) for additional endpoints that might inherit the same validation bugs. 94 95 ### Redirect token leakage on allowlisted domains with attacker-controlled subpaths 96 97 Locking `redirect_uri` to “owned/first-party domains” doesn’t help if any allowlisted domain exposes **attacker-controlled paths or execution contexts** (legacy app platforms, user namespaces, CMS uploads, etc.). If the OAuth/federated login flow **returns tokens in the URL** (query or hash), an attacker can:<sup>[[1]](#references)</sup> 98 99 1. Start a legitimate flow to mint a pre-token (e.g., an `etoken` in a multi-step Accounts Center/FXAuth flow). 100 2. Send the victim an authorization URL that sets the allowlisted domain as `redirect_uri`/`base_uri` but points `next`/path into an attacker-controlled namespace (e.g., `https://apps.facebook.com/<attacker_app>`). 101 3. After the victim approves, the IdP redirects to the attacker-controlled path with sensitive values in the URL (`token`, `blob`, codes, etc.). 102 4. JavaScript on that page reads `window.location` and exfiltrates the values despite the domain being “trusted.” 103 5. Replay the captured values against downstream privileged endpoints that only expect the redirect-carried tokens. Examples from the FXAuth flow: 104 105 ```text 106 # Account linking without further prompts 107 https://accountscenter.facebook.com/add/?auth_flow=frl_linking&blob=<BLOB>&token=<TOKEN> 108 109 # Reauth-gated actions (e.g., profile updates) without user confirmation 110 https://accountscenter.facebook.com/profiles/<VICTIM_ID>/name/?auth_flow=reauth&blob=<BLOB>&token=<TOKEN> 111 ``` 112 113 ### XSS in redirect implementation <a href="#bda5" id="bda5"></a> 114 115 As mentioned in this bug bounty report [https://blog.dixitaditya.com/2021/11/19/account-takeover-chain.html](https://blog.dixitaditya.com/2021/11/19/account-takeover-chain.html) it might be possible that the redirect **URL is being reflected in the response** of the server after the user authenticates, being **vulnerable to XSS**.<sup>[[16]](#references)</sup> Possible payload to test: 116 117 ```text 118 https://app.victim.com/login?redirectUrl=https://app.victim.com/dashboard</script><h1>test</h1> 119 ``` 120 121 ### OAuth callback error pages: reflected `error_description`, trusted-origin phishing, and encoded `state` leakage 122 123 Some OAuth integrations use a **first-party callback page** to render login failures after the IdP redirects the browser back. These pages are high value because they already run on a **trusted origin** and often consume attacker-controlled parameters such as `error`, `error_description`, `message`, `description`, or `state`.<sup>[[8]](#references)</sup> 124 125 - **Reflecting `error_description` into HTML** without strict output encoding turns the callback into a **trusted-origin phishing page**. Even when `<script>` is filtered, HTML injection can still spoof the entire failure page and instruct the victim to perform attacker-chosen actions. 126 - **WAFs often key on common handlers** such as `onload`/`onerror`. When normal payloads are blocked, try **browser-specific or uncommon events** that defenders may not blacklist. A practical example is Safari's `onpagereveal`, which can execute when the malicious callback page is shown in Safari:<sup>[[9]](#references)</sup> 127 128 ```html 129 <body onpagereveal=open("https://attacker.example")> 130 This step can only be completed in Safari 131 ``` 132 133 - **Test self-referential payloads**: if the injected HTML/JS can reopen or reload the same callback URL, you may get **client-side resource exhaustion**, repeated popups/tabs, or **log flooding** on every render. 134 - **Always decode opaque-looking `state` values**. Many implementations Base64-encode JSON or user metadata and assume that is "hidden". Base64 is reversible, so callback URLs may leak **PII** such as email addresses, tenant identifiers, return paths, or internal workflow state. 135 - **Treat URL exposure as part of the bug**: anything placed in the callback URL can later appear in browser history, reverse proxies, load balancers, app logs, monitoring tools, screenshots, and `Referer` headers if the page loads third-party resources. 136 137 Quick checks during testing: 138 139 1. Trigger both success and failure OAuth callbacks and capture the full URL plus rendered HTML. 140 2. Replay the callback while mutating `error_description`, `message`, and similar error fields with plain text, HTML, and event-handler payloads. 141 3. Decode `state` as Base64/URL-safe Base64 and inspect it for PII or application state that should have stayed server-side. 142 4. Repeat browser-specific payloads in Safari/WebKit when the WAF blocks standard inline-event XSS probes. 143 144 ### CSRF - Improper handling of state parameter <a href="#bda5" id="bda5"></a> 145 146 The `state` parameter is the Authorization Code flow CSRF token: the client must generate a **cryptographically random value per browser instance**, persist it somewhere only that browser can read (cookie, local storage, etc.), send it in the authorization request, and reject any response that does not return the same value. Whenever the value is static, predictable, optional, or not tied to the user’s session, the attacker can finish their own OAuth flow, capture the final `?code=` request (without sending it), and later coerce a victim browser into replaying that request so the victim account becomes linked to the attacker’s identity provider profile.<sup>[[5]](#references)</sup> 147 148 The replay pattern is always the same: 149 150 1. The attacker authenticates against the IdP with their account and intercepts the last redirect containing `code` (and any `state`). 151 2. They drop that request, keep the URL, and later abuse any CSRF primitive (link, iframe, auto-submitting form) to force the victim browser to load it. 152 3. If the client does not enforce `state`, the application consumes the attacker’s authorization result and logs the attacker into the victim’s app account. 153 154 A practical checklist for `state` handling during tests: 155 156 - **Missing `state` entirely** – if the parameter never appears, the whole login is CSRFable. 157 - **`state` not required** – remove it from the initial request; if the IdP still issues codes that the client accepts, the defense is opt-in. 158 - **Returned `state` not validated** – tamper with the value in the response (Burp, MITM proxy). Accepting mismatched values means the stored token is never compared. 159 - **Predictable or purely data-driven `state`** – many apps stuff redirect paths or JSON blobs into `state` without mixing in randomness, letting attackers guess valid values and replay flows. Always prepend/append strong entropy before encoding data. 160 - **`state` fixation** – if the app lets users supply the `state` value (e.g., via crafted authorization URLs) and reuses it throughout the flow, an attacker can lock in a known value and reuse it across victims. 161 162 PKCE can complement `state` (especially for public clients) by binding the authorization code to a code verifier, but web clients must still track `state` to prevent cross-user CSRF/account-linking bugs. 163 164 ### Pre Account Takeover <a href="#ebe4" id="ebe4"></a> 165 166 1. **Without Email Verification on Account Creation**: Attackers can preemptively create an account using the victim's email. If the victim later uses a third-party service for login, the application might inadvertently link this third-party account to the attacker's pre-created account, leading to unauthorized access.<sup>[[2]](#references)</sup> 167 2. **Exploiting Lax OAuth Email Verification**: Attackers may exploit OAuth services that don't verify emails by registering with their service and then changing the account email to the victim's. This method similarly risks unauthorized account access, akin to the first scenario but through a different attack vector. 168 169 ### Disclosure of Secrets <a href="#e177" id="e177"></a> 170 171 The `client_id` is intentionally public, but the **`client_secret` must never be recoverable by end users**. Authorization Code deployments that embed the secret in **mobile APKs, desktop clients, or single-page apps** effectively hand that credential to anyone who can download the package. Always inspect public clients by:<sup>[[5]](#references)</sup> 172 173 - Unpacking the APK/IPA, desktop installer, or Electron app and grepping for `client_secret`, Base64 blobs that decode to JSON, or hard-coded OAuth endpoints. 174 - Reviewing bundled config files (plist, JSON, XML) or decompiled strings for client credentials. 175 176 Once the attacker extracts the secret they only need to steal any victim authorization `code` (via a weak `redirect_uri`, logs, etc.) to independently hit `/token` and mint access/refresh tokens without involving the legitimate app. Treat public/native clients as **incapable of holding secrets**—they should instead rely on PKCE (RFC 7636) to prove possession of a per-instance code verifier instead of a static secret. During testing, confirm whether PKCE is mandatory and whether the backend actually rejects token exchanges that omit either the `client_secret` **or** a valid `code_verifier`. 177 178 ### Client Secret Bruteforce 179 180 You can try to **bruteforce the client_secret** of a service provider with the identity provider in order to be try to steal accounts.<sup>[[2]](#references)</sup>\ 181 The request to BF may look similar to: 182 183 ```text 184 POST /token HTTP/1.1 185 content-type: application/x-www-form-urlencoded 186 host: 10.10.10.10:3000 187 content-length: 135 188 Connection: close 189 190 code=77515&redirect_uri=http%3A%2F%2F10.10.10.10%3A3000%2Fcallback&grant_type=authorization_code&client_id=public_client_id&client_secret=[bruteforce] 191 ``` 192 193 ### Referer/Header/Location artifacts leaking Code + State 194 195 Once the client has the **code and state**, if they surface in **`location.href`** or **`document.referrer`** and are forwarded to third parties, they leak.<sup>[[7]](#references)</sup> Two recurring patterns: 196 197 - **Classic Referer leak**: after the OAuth redirect, any navigation that keeps `?code=&state=` in the URL will push them into the **Referer** header sent to CDNs/analytics/ads. 198 - **Telemetry/analytics confused deputy**: some SDKs (pixels/JS loggers) react to `postMessage` events and then **send the current `location.href`/`referrer` to backend APIs using a token supplied in the message**. If you can inject your own token into that flow (e.g., via an attacker-controlled postMessage relay), you can later read the SDK’s API request history/logs and recover the victim’s OAuth artifacts embedded in those requests. 199 200 ### Access Token Stored in Browser History 201 202 The core guarantee of the Authorization Code grant is that **access tokens never reach the resource owner’s browser**. When implementations leak tokens client-side, any minor bug (XSS, Referer leak, proxy logging) becomes instant account compromise.<sup>[[5]](#references)</sup> Always check for: 203 204 - **Tokens in URLs** – if `access_token` appears in the query/fragment, it lands in browser history, server logs, analytics, and Referer headers sent to third parties. 205 - **Tokens transiting untrusted middleboxes** – returning tokens over HTTP or through debugging/corporate proxies lets network observers capture them directly. 206 - **Tokens stored in JavaScript state** – React/Vue stores, global variables, or serialized JSON blobs expose tokens to every script on the origin (including XSS payloads or malicious extensions). 207 - **Tokens persisted in Web Storage** – `localStorage`/`sessionStorage` retain tokens long after logout on shared devices and are script-accessible. 208 209 Any of these findings usually upgrades otherwise “low” bugs (like a CSP bypass or DOM XSS) into full API takeover because the attacker can simply read and replay the leaked bearer token. 210 211 ### Frontend-only SSO gates: MSAL/OAuth login spoofing, `localStorage` identity injection, and pre-auth session bootstrap 212 213 Some SPAs use **MSAL/OIDC only as a client-side route guard**: the bundle checks `getAllAccounts()` (or equivalent), writes identity fields into `localStorage`, and renders the “private” UI. If the backend **doesn't validate the bearer token server-side** on every API call, patching the JavaScript is enough to expose protected routes and prove missing authorization.<sup>[[13]](#references)</sup> 214 215 Practical workflow during testing: 216 217 1. **Download the JS bundle before/around the SSO redirect**. Even when the browser immediately jumps to Microsoft login, React/Next/Vue assets often load first and disclose hidden routes, API names, storage keys, role strings, and session bootstrap endpoints. 218 2. **Patch the account-check logic locally** so the SPA believes one user is signed in (e.g. force the MSAL account lookup to return a non-empty list or short-circuit the redirect branch). This should only unlock client rendering; it must not grant server data by itself. 219 3. **Replay the underlying APIs without the real token**: remove the `Authorization` header, replace it with garbage, or call the endpoint directly outside the browser. If responses still succeed, the backend is not enforcing bearer-token validation. 220 4. **Treat browser storage as attacker-controlled**. If the app stores `username`, employee IDs, tenant/company IDs, `role`, `isAdmin`, or similar values in `localStorage`/`sessionStorage`, overwrite them with another valid user's values and reload. Any privilege change means the server is trusting client-supplied identity state. 221 5. **Inspect session bootstrap code**. A common follow-on bug is an unauthenticated `GET` that returns a session GUID / opaque session ID plus an expiry timestamp. If dropping those values into storage is enough to “complete” login, the session is being minted pre-auth and is not bound server-side to the authenticated identity. 222 6. **Check for static secrets shipped to the browser** such as `x-api-key`, AWS API Gateway keys, or encrypted constants that the same bundle later decrypts. These values are **public** once delivered to the client and cannot prove who the user is. 223 224 High-signal indicators: 225 226 - Sensitive APIs work with **no bearer token**, a **tampered bearer token**, or only a **browser-exposed static API key**. 227 - The SPA derives identity from `localStorage` and the API accepts those values as truth. 228 - Session creation happens through an **unauthenticated bootstrap endpoint** that returns a reusable GUID/opaque token. 229 - “Client-side encryption” is used to hide API keys, usernames, or session material inside the bundle. If the browser can decrypt it, the attacker can too. 230 231 For Microsoft Entra / MSAL-backed APIs, the resource server must validate at least the **signature**, **issuer**, **audience**, **expiration**, and then enforce the expected **scopes / app roles / tenant authorization** before returning any sensitive data.<sup>[[14]](#references)</sup><sup>[[15]](#references)</sup> 232 233 ### Everlasting Authorization Code 234 235 Authorization codes must be **short-lived, single-use, and replay-aware**.<sup>[[5]](#references)</sup> When assessing a flow, capture a `code` and: 236 237 - **Test the lifetime** – RFC 6749 recommends minutes, not hours. Try redeeming the code after 5–10 minutes; if it still works, the exposure window for any leaked code is excessive. 238 - **Test sequential reuse** – send the same `code` twice. If the second request yields another token, attackers can clone sessions indefinitely. 239 - **Test concurrent redemption/race conditions** – fire two token requests in parallel (Burp intruder, turbo intruder). Weak issuers sometimes grant both. 240 - **Observe replay handling** – a reuse attempt should not only fail but also revoke any tokens already minted from that code. Otherwise, a detected replay leaves the attacker’s first token active. 241 242 Combining a replay-friendly code with any `redirect_uri` or logging bug allows persistent account access even after the victim completes the legitimate login. 243 244 ### Authorization/Refresh Token not bound to client 245 246 If you can get the **authorization code** and **redeem it for a different client/app**, you can takeover other accounts.<sup>[[7]](#references)</sup> Test for weak binding by: 247 248 - Capturing a `code` for **app A** and sending it to **app B’s token endpoint**; if you still receive a token, audience binding is broken. 249 - Trying first-party token minting endpoints that should be restricted to their own client IDs; if they accept arbitrary `state`/`app_id` while only validating the code, you effectively perform an **authorization-code swap** to mint higher-privileged first-party tokens. 250 - Checking whether client binding ignores nonce/redirect URI mismatches. If an error page still loads SDKs that log `location.href`, combine with Referer/telemetry leaks to steal codes and redeem them elsewhere. 251 252 Any endpoint that exchanges `code` → token **must** verify the issuing client, redirect URI, and nonce; otherwise, a stolen code from any app can be upgraded to a first-party access token. 253 254 ### Happy Paths, XSS, Iframes & Post Messages to leak code & state values 255 256 [**Check this post**](https://labs.detectify.com/writeups/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/#gadget-2-xss-on-sandbox-third-party-domain-that-gets-the-url)<sup>[[17]](#references)</sup> 257 258 ### AWS Cognito <a href="#bda5" id="bda5"></a> 259 260 In this bug bounty report: [**https://security.lauritz-holtmann.de/advisories/flickr-account-takeover/**](https://security.lauritz-holtmann.de/advisories/flickr-account-takeover/) you can see that the **token** that **AWS Cognito** gives back to the user might have **enough permissions to overwrite the user data**. Therefore, if you can **change the user email for a different user email**, you might be able to **take over** others accounts.<sup>[[18]](#references)</sup> 261 262 ```bash 263 # Read info of the user 264 aws cognito-idp get-user --region us-east-1 --access-token eyJraWQiOiJPVj[...] 265 266 # Change email address 267 aws cognito-idp update-user-attributes --region us-east-1 --access-token eyJraWQ[...] --user-attributes Name=email,Value=imaginary@flickr.com 268 { 269 "CodeDeliveryDetailsList": [ 270 { 271 "Destination": "i***@f***.com", 272 "DeliveryMedium": "EMAIL", 273 "AttributeName": "email" 274 } 275 ] 276 } 277 ``` 278 279 For more detailed info about how to abuse AWS Cognito check [AWS Cognito - Unauthenticated Enum Access](https://cloud.hacktricks.wiki/en/pentesting-cloud/aws-security/aws-unauthenticated-enum-access/aws-cognito-unauthenticated-enum.html). 280 281 ### Abusing other Apps tokens <a href="#bda5" id="bda5"></a> 282 283 As [**mentioned in this writeup**](https://salt.security/blog/oh-auth-abusing-oauth-to-take-over-millions-of-accounts), OAuth flows that expect to receive the **token** (and not a code) could be vulnerable if they not check that the token belongs to the app.<sup>[[19]](#references)</sup> 284 285 This is because an **attacker** could create an **application supporting OAuth and login with Facebook** (for example) in his own application. Then, once a victim logins with Facebook in the **attackers application**, the attacker could get the **OAuth token of the user given to his application, and use it to login in the victim OAuth application using the victims user token**. 286 287 > [!CAUTION] 288 > Therefore, if the attacker manages to get the user access his own OAuth application, he will be able to take over the victims account in applications that are expecting a token and aren't checking if the token was granted to their app ID. 289 290 ### Two links & cookie <a href="#bda5" id="bda5"></a> 291 292 According to [**this writeup**](https://medium.com/@metnew/why-electron-apps-cant-store-your-secrets-confidentially-inspect-option-a49950d6d51f), an attacker could make a victim open a page whose **`returnUrl`** pointed to the attacker's host. The value was stored in an `RU` cookie, and a later prompt asked whether the user wanted to grant that host access.<sup>[[20]](#references)</sup> 293 294 The attacker could initiate the **OAuth flow** in one tab to set the `RU` cookie, close it before the warning appeared, and open a new tab without the visible `returnUrl`. The new prompt omitted the attacker's host even though the cookie still caused the token to be redirected there. 295 296 ### Prompt Interaction Bypass <a href="#bda5" id="bda5"></a> 297 298 As explained in [**this video**](https://www.youtube.com/watch?v=n9x7_J_a_7Q), some OAuth implementations accept **`prompt=none`** to avoid interactive confirmation when the user is already logged in.<sup>[[21]](#references)</sup> 299 300 ### response_mode 301 302 As [**explained in this video**](https://www.youtube.com/watch?v=n9x7_J_a_7Q), **`response_mode`** can select how the authorization response is returned:<sup>[[21]](#references)</sup> 303 304 - `response_mode=query` -> The code is provided inside a GET parameter: `?code=2397rf3gu93f` 305 - `response_mode=fragment` -> The code is provided inside the URL fragment parameter `#code=2397rf3gu93f` 306 - `response_mode=form_post` -> The code is provided inside a POST form with an input called `code` and the value 307 - `response_mode=web_message` -> The code is sent in a postMessage payload: `window.opener.postMessage({"code": "asdasdasd...` 308 309 ### Clickjacking OAuth consent dialogs 310 311 OAuth consent/login dialogs are ideal clickjacking targets: if they can be framed, an attacker can overlay custom graphics, hide the real buttons, and trick users into approving dangerous scopes or linking accounts.<sup>[[5]](#references)</sup> Build PoCs that: 312 313 1. Load the IdP authorization URL inside an `<iframe sandbox="allow-forms allow-scripts allow-same-origin">`. 314 2. Use absolute positioning/opacity tricks to align fake buttons with the hidden **Allow**/**Approve** controls. 315 3. Optionally pre-fill parameters (scopes, redirect URI) so the stolen approval immediately benefits the attacker. 316 317 During testing verify that IdP pages emit either `X-Frame-Options: DENY/SAMEORIGIN` or a restrictive `Content-Security-Policy: frame-ancestors 'none'`. If neither is present, demonstrate the risk with tooling like [NCC Group’s clickjacking PoC generator](https://github.com/nccgroup/clickjacking-poc) and record how easily a victim authorizes the attacker’s app. For additional payload ideas see [Clickjacking](/hacktricks/pentesting-web/clickjacking). 318 319 ### OAuth ROPC flow - 2 FA bypass <a href="#b440" id="b440"></a> 320 321 According to [**this blog post**](https://cybxis.medium.com/a-bypass-on-gitlabs-login-email-verification-via-oauth-ropc-flow-e194242cad96), the Resource Owner Password Credentials flow accepts a **username and password** directly. If that path returns a broadly privileged token without enforcing the account's second factor, the token can bypass 2FA.<sup>[[22]](#references)</sup> 322 323 ### ATO on web page redirecting based on open redirect to referrer <a href="#bda5" id="bda5"></a> 324 325 This [**blogpost**](https://blog.voorivex.team/oauth-non-happy-path-to-ato) comments how it was possible to abuse an **open redirect** to the value from the **referrer** to abuse OAuth to ATO.<sup>[[23]](#references)</sup> The attack was: 326 327 1. The victim visits the attacker's web page. 328 2. The victim opens a malicious link, and a new window starts the Google OAuth flow with `response_type=id_token,code&prompt=none`, retaining the **attacker's site as the referrer**. 329 3. In the opener, after the provider authorizes the victim, it sends them back to the value of the `redirect_uri` parameter (victim web) with 30X code which still keeps the attackers website in the referer. 330 4. The victim site **triggers its referrer-based open redirect** back to the attacker's site. Because **`response_type`** was **`id_token,code`**, the fragment carries the authorization response to the attacker, enabling account takeover on the victim site. 331 332 ### Open Dynamic Client Registration + PKCE-enabled malicious client flows 333 334 If the authorization server exposes `/.well-known/oauth-authorization-server` or `/.well-known/openid-configuration`, inspect it for a **`registration_endpoint`**, supported grant types, and `token_endpoint_auth_methods_supported`. A combination such as **dynamic client registration + `authorization_code` + `none` client auth + PKCE** means the platform may allow attacker-created **public clients**.<sup>[[12]](#references)</sup> 335 336 Quick recon: 337 338 ```bash 339 curl -s https://target/.well-known/oauth-authorization-server 340 ``` 341 342 Interesting signals in the metadata: 343 344 - `registration_endpoint` is reachable from the internet. 345 - `token_endpoint_auth_methods_supported` contains `none`. 346 - `code_challenge_methods_supported` contains `S256`. 347 348 If `POST /register` is unauthenticated, try registering an attacker-controlled callback: 349 350 ```json 351 {"redirect_uris":["https://attacker.com/callback"]} 352 ``` 353 354 This is **more dangerous than a normal open `redirect_uri` bug** because the attacker becomes a **legitimate client** from the IdP perspective. After that, the workflow is: 355 356 1. Register the malicious client and keep the returned `client_id` (and `client_secret` if any). 357 2. Generate your own PKCE `code_verifier` / `code_challenge` pair. 358 3. Send the victim to a normal OAuth authorize URL using your `client_id` and `redirect_uri`. 359 4. If the victim approves the consent screen, the authorization server sends **their** code to **your** callback. 360 5. Exchange the code with your known verifier and obtain victim tokens. 361 362 PKCE does **not** stop this pattern when the attacker controls the entire OAuth client lifecycle. PKCE protects against **code interception by a third party**; it does not protect against a **malicious registered client** that generated the `code_challenge` itself. 363 364 Extra checks during testing: 365 366 - The registration endpoint should require authentication, approval, or ownership validation for `redirect_uris`. 367 - `redirect_uris` should be matched exactly and not accept arbitrary attacker domains. 368 - If the metadata advertises public-client token auth (`none`), verify whether this is only allowed for trusted clients and whether token redemption still enforces the correct `client_id`, `redirect_uri`, and PKCE verifier. 369 - If the authorization API returns authorization data or a redirect target **before** a user session is established, treat that as a broken authorization/authentication boundary and chain it with the malicious-client flow above. 370 - Wildcard CORS on OAuth endpoints is not usually the root cause of the account takeover, but it can expand browser-based abuse and should be reported as an impact amplifier. 371 372 ### SSRFs parameters <a href="#bda5" id="bda5"></a> 373 374 [**Check this research**](https://portswigger.net/research/hidden-oauth-attack-vectors) **For further details of this technique.**<sup>[[3]](#references)</sup> 375 376 Dynamic Client Registration in OAuth serves as a less obvious but critical vector for security vulnerabilities, specifically for **Server-Side Request Forgery (SSRF)** attacks. This endpoint allows OAuth servers to receive details about client applications, including sensitive URLs that could be exploited. 377 378 **Key Points:** 379 380 - **Dynamic Client Registration** is often mapped to `/register` and accepts details like `client_name`, `client_secret`, `redirect_uris`, and URLs for logos or JSON Web Key Sets (JWKs) via POST requests. 381 - This feature adheres to specifications laid out in **RFC7591** and **OpenID Connect Registration 1.0**, which include parameters potentially vulnerable to SSRF. 382 - The registration process can inadvertently expose servers to SSRF in several ways: 383 - **`logo_uri`**: A URL for the client application's logo that might be fetched by the server, triggering SSRF or leading to XSS if the URL is mishandled. 384 - **`jwks_uri`**: A URL to the client's JWK document, which if maliciously crafted, can cause the server to make outbound requests to an attacker-controlled server. 385 - **`sector_identifier_uri`**: References a JSON array of `redirect_uris`, which the server might fetch, creating an SSRF opportunity. 386 - **`request_uris`**: Lists allowed request URIs for the client, which can be exploited if the server fetches these URIs at the start of the authorization process. 387 388 **Exploitation Strategy:** 389 390 - SSRF can be triggered by registering a new client with malicious URLs in parameters like `logo_uri`, `jwks_uri`, or `sector_identifier_uri`. 391 - While direct exploitation via `request_uris` may be mitigated by whitelist controls, supplying a pre-registered, attacker-controlled `request_uri` can facilitate SSRF during the authorization phase. 392 393 ### OAuth/OIDC Discovery URL Abuse & OS Command Execution 394 395 Research on [CVE-2025-6514](https://amlalabs.com/blog/oauth-cve-2025-6514/) (impacting `mcp-remote` clients such as Claude Desktop, Cursor or Windsurf) shows how **dynamic OAuth discovery becomes an RCE primitive** whenever the client forwards IdP metadata straight to the operating system. The remote MCP server returns an attacker-controlled `authorization_endpoint` during the discovery exchange (`/.well-known/openid-configuration` or any metadata RPC). `mcp-remote ≤0.1.15` would then call the system URL handler (`start`, `open`, `xdg-open`, etc.) with whatever string arrived, so any scheme/path supported by the OS executed locally.<sup>[[6]](#references)</sup> 396 397 **Attack workflow** 398 399 1. Point the desktop agent to a hostile MCP/OAuth server (`npx mcp-remote https://evil`). The agent receives `401` plus metadata. 400 2. The server answers with JSON such as: 401 402 ```text 403 HTTP/1.1 200 OK 404 Content-Type: application/json 405 406 { 407 "authorization_endpoint": "file:/c:/windows/system32/calc.exe", 408 "token_endpoint": "https://evil/idp/token", 409 ... 410 } 411 ``` 412 413 3. The client launches the OS handler for the supplied URI. Windows accepts payloads like `file:/c:/windows/system32/calc.exe /c"powershell -enc ..."`; macOS/Linux accept `file:///Applications/Calculator.app/...` or even custom schemes such as `cmd://bash -lc '<payload>'` if registered. 414 4. Because this happens before any user interaction, **merely configuring the client to talk to the attacker server yields code execution**. 415 416 **How to test** 417 418 - Target any OAuth-capable desktop/agent that performs discovery over HTTP(S) and opens returned endpoints locally (Electron apps, CLI helpers, thick clients). 419 - Intercept or host the discovery response and replace `authorization_endpoint`, `device_authorization_endpoint`, or similar fields with `file://`, `cmd://`, UNC paths, or other dangerous schemes. 420 - Observe whether the client validates the scheme/host. Lack of validation results in immediate execution under the user context and proves the issue. 421 - Repeat with different schemes to map the full attack surface (e.g., `ms-excel:`, `data:text/html,`, custom protocol handlers) and demonstrate cross-platform reach. 422 423 ## OAuth providers Race Conditions 424 425 If the platform you are testing is an OAuth provider [**read this to test for possible Race Conditions**](/hacktricks/pentesting-web/race-condition). 426 427 ## Mutable Claims Attack 428 429 In OAuth, the sub field uniquely identifies a user, but its format varies by Authorization Server. To standardize user identification, some clients use emails or user handles. However, this is risky because: 430 431 - Some Authorization Servers do not ensure that these properties (like email) remain immutable. 432 - In certain implementations—such as **"Login with Microsoft"**—the client relies on the email field, which is **user-controlled by the user in Entra ID** and not verified. 433 - An attacker can exploit this by creating their own Azure AD organization (e.g., doyensectestorg) and using it to perform a Microsoft login. 434 - Even though the Object ID (stored in sub) is immutable and secure, the reliance on a mutable email field can enable an account takeover (for example, hijacking an account like victim@gmail.com).<sup>[[4]](#references)</sup> 435 436 A common variant appears when the **OAuth/OIDC provider itself accepts arbitrary unverified emails** during local signup and later returns that same `email` to relying parties during federated login. This turns a seemingly harmless product decision ("verify email later") into a **combinatorial bug** once third-party sites trust the provider's identity claims:<sup>[[10]](#references)</sup> 437 438 1. The attacker creates an account at the provider with `victim@company.com` but never proves mailbox ownership. 439 2. A relying party allows "Login with Provider" and links/logs in users by matching the returned `email`. 440 3. The attacker authenticates to the provider and completes the OAuth flow. 441 4. The relying party receives `email=victim@company.com` and binds the session to the victim's local account. 442 443 Practical testing notes: 444 445 - As a **provider**, verify whether the platform returns `email` for accounts that never completed verification, or fails to expose `email_verified=false` in the ID Token / UserInfo response. 446 - As a **relying party**, test whether an existing local account can be accessed by sending an unverified `email` claim from a lab IdP or from a real provider with lax verification. 447 - Check whether the application binds identity to **`iss` + `sub`** (stable) or to `email` / `preferred_username` (mutable, recycled, or unverified). 448 - Review auto-linking logic separately from first-time signup: many apps reject new unverified users but still **merge into existing accounts** on email match alone. 449 450 > [!CAUTION] 451 > Per OpenID Connect, `email_verified=true` is the flag that tells the RP the provider actually verified mailbox control, and `iss` + `sub` are the only stable user identifiers. If the provider omits verification state or the RP ignores it, treat the email as untrusted until a **local verification step** completes.<sup>[[11]](#references)</sup> 452 453 ## Client Confusion Attack 454 455 In a **Client Confusion Attack**, an application using the OAuth Implicit Flow fails to verify that the final access token is specifically generated for its own Client ID. An attacker sets up a public website that uses Google’s OAuth Implicit Flow, tricking thousands of users into logging in and thereby harvesting access tokens intended for the attacker’s site. If these users also have accounts on another vulnerable website that does not validate the token's Client ID, the attacker can reuse the harvested tokens to impersonate the victims and take over their accounts.<sup>[[4]](#references)</sup> 456 457 ## Scope Upgrade Attack 458 459 The **Authorization Code Grant** type involves secure server-to-server communication for transmitting user data. However, if the **Authorization Server** implicitly trusts a scope parameter in the Access Token Request (a parameter not defined in the RFC), a malicious application could upgrade the privileges of an authorization code by requesting a higher scope. After the **Access Token** is generated, the **Resource Server** must verify it: for JWT tokens, this involves checking the JWT signature and extracting data such as client_id and scope, while for random string tokens, the server must query the Authorization Server to retrieve the token’s details.<sup>[[4]](#references)</sup> 460 461 ## Redirect Scheme Hijacking 462 463 In mobile OAuth implementations, apps use **custom URI schemes** to receive redirects with Authorization Codes. However, because multiple apps can register the same scheme on a device, the assumption that only the legitimate client controls the redirect URI is violated. On Android, for instance, an Intent URI like `com.example.app://` oauth is caught based on the scheme and optional filters defined in an app’s intent-filter. Since Android’s intent resolution can be broad—especially if only the scheme is specified—an attacker can register a malicious app with a carefully crafted intent filter to hijack the authorization code. This can **enable an account takeover** either through user interaction (when multiple apps are eligible to handle the intent) or via bypass techniques that exploit overly specific filters, as detailed by Ostorlab's assessment flowchart.<sup>[[4]](#references)</sup> 464 465 ## References 466 467 - [1] [Leaking FXAuth token via allowlisted Meta domains](https://ysamm.com/uncategorized/2026/01/16/leaking-fxauth-token.html) 468 - [2] [The wonderful world of OAuth (bug bounty edition)](https://medium.com/a-bugz-life/the-wondeful-world-of-oauth-bug-bounty-edition-af3073b354c1) 469 - [3] [Hidden OAuth attack vectors](https://portswigger.net/research/hidden-oauth-attack-vectors) 470 - [4] [OAuth common vulnerabilities](https://blog.doyensec.com/2025/01/30/oauth-common-vulnerabilities.html) 471 - [5] [An Offensive Guide to the OAuth 2.0 Authorization Code Grant](https://www.nccgroup.com/research-blog/an-offensive-guide-to-the-authorization-code-grant/) 472 - [6] [OAuth Discovery as an RCE Vector (Amla Labs)](https://amlalabs.com/blog/oauth-cve-2025-6514/) 473 - [7] [Leaking fbevents: OAuth code exfiltration via postMessage trust leading to Instagram ATO](https://ysamm.com/uncategorized/2026/01/16/leaking-fbevents-ato.html) 474 - [8] [Rapid7: CVE-2026-31381, CVE-2026-31382: Gainsight Assist Information Disclosure and Cross-Site Scripting (FIXED)](https://www.rapid7.com/blog/post/ve-cve-2026-31381-cve-2026-31382-gainsight-assist-information-disclosure-xss-fixed) 475 - [9] [MDN: Window `pagereveal` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/pagereveal_event) 476 - [10] [The Most Dangerous OAuth Bug I’ve Ever Found](https://medium.com/@AliMojaver/the-most-dangerous-oauth-bug-ive-ever-found-a2af1275385c) 477 - [11] [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) 478 - [12] [How I Found a Critical OAuth Misconfiguration That Led to Account Takeover](https://medium.com/@iamshafayat/how-i-found-a-critical-oauth-misconfiguration-that-led-to-account-takeover-abfec43eaea6) 479 - [13] [Exploiting vulnerabilities in Johnson & Johnson web apps](https://eaton-works.com/2026/06/24/jnj-webapp-hacks/) 480 - [14] [Access tokens in the Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens) 481 - [15] [Protected web API: Verify scopes and app roles](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-protected-web-api-verification-scope-app-roles) 482 - [16] [Account Takeover Chain – XSS in an OAuth redirect reflection](https://blog.dixitaditya.com/2021/11/19/account-takeover-chain.html) 483 - [17] [Account hijacking using "Dirty Dancing" in sign-in OAuth flows](https://labs.detectify.com/writeups/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/#gadget-2-xss-on-sandbox-third-party-domain-that-gets-the-url) 484 - [18] [Flickr account takeover advisory](https://security.lauritz-holtmann.de/advisories/flickr-account-takeover/) 485 - [19] [Oh-Auth: Abusing OAuth to take over millions of accounts](https://salt.security/blog/oh-auth-abusing-oauth-to-take-over-millions-of-accounts) 486 - [20] [Why Electron apps can’t store your secrets confidentially](https://medium.com/@metnew/why-electron-apps-cant-store-your-secrets-confidentially-inspect-option-a49950d6d51f) 487 - [21] [OAuth prompt=none and response_mode attack techniques (video)](https://www.youtube.com/watch?v=n9x7_J_a_7Q) 488 - [22] [A bypass on GitLab’s login email verification via OAuth ROPC flow](https://cybxis.medium.com/a-bypass-on-gitlabs-login-email-verification-via-oauth-ropc-flow-e194242cad96) 489 - [23] [OAuth Non-Happy Path to ATO](https://blog.voorivex.team/oauth-non-happy-path-to-ato) 490 - [24] [RFC 6749 – The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749)