Hacking HTTP CORS from inside out: a theory to practice approach
Hi, there. Hope all of you are fine. Today, we are going to dissect some web application security controls.
So, if you ever wondered about the HTTP CORS (Cross-Origin Resource Sharing) inner workings, or never heard about it before, but feel it like a vital web application concept to grasp. In either case, grab a cup of coffee and make yourself at home.
In the next sections, we will dive into how browsers and servers handle CORS rules. Using a theoretical and hands-on approach, I expect to bring you some light into how web resources should be securely configured to be trusted.
Summarily, we will be covering the following topics:
- HTTP CORS fundamentals contemplating Simple versus Preflight HTTP Requests, Same Origin Policy, CORS standard headers, misconfigurations, etc.;
- Deep dive into a Docker-based containerized environment where you can analyze the application source code and compare it to the expected behavior ;
- Understand the HTTP request-response cycle by practicing the example scenarios;
- How to bypass CORS-protected resources using a manual proxy interception tool;
- Be introduced to an API automation CORS-related project and other useful sources of information.
First things first
Same Origin Policy (SOP)
Before we get to the CORS matter, it is essential to comprehend an underlying concept of web resources access protection. I’m talking about the Same Origin Policy, also known for the acronym SOP.
Built by Netscape way back in 1995, the SOP concept is now shipped within all major current web browsers.
As to avoid unauthorized use of resources by external parties, the browsers count on an origin-based restriction policy. Formally, the said external parties are identified by their origin (domain) and accessed through URLs.
A document held by a given origin A will only be accessible by other documents from the SAME origin A. This rule is valid if the SOP is effectively in place (and depends on the browser implementation). This policy aims to reduce the chances of a possible attack vector.
But let’s depict what an origin looks like. One origin could be understood as the tuple:
- <schema (or protocol)> — <hostname> — <port>
So two URLs represent the same origin only if they share the same tuple. Examples of origin variations and categories for the URL `http://hacking.cors.com/mr_robots.txt`:
Additionally, keep in mind that the SOP allows inter-origin HTTP requests with GET and POST methods. But it forbids inter-origin PUT and DELETE. Besides that, sending custom HTTP headers is permitted only in the context of inter-origin requests, being denied to foreigners .
There are some different types of Same Origin Policy. In practice, they are applied under particular rules given the technology in place.
So if we take SOP as a sandboxing concept, it becomes reasonable to have the same origin implementations for other web technologies as well. It is the case of cookies, Java, old-R.I.P Flash, and their respective SOP policy feature.
HTTP CORS Fundamentals
The SOP technology was certainly a considerable jump towards more secure web applications. But it applies the deny rule by default.
The true nature of the World Wide Web is that pages could communicate with other resources all across the network. Web applications are meant to access external resources.
So how do we relax the same-origin rules while maintaining the security access of restricted resources? You got it: CORS.
In simple terms, Cross-Origin Resource Sharing allows the pages from a specific domain/origin to consume the resources from another domain/origin. The consent of who can access a resource is the resource’s owner (server) responsibility.
The browser-server trust relationship takes form through a family of CORS HTTP Headers. In the average case, they are added to the HTTP requests and responses by the resource’s web server (like Nginx, Apache, IIS, etc.), by the application, or the browser. The CORS headers then instruct the soliciting browser whether or not to trust the origin and proceed with the response processing.
Let’s take a breath here. Note that the browser fully trusts the response returned from the server. Keep this in mind.
In contrast to Preflight requests, there are simple ones. Simple requests are those who respect the following conditions:
Preflight HTTP Request
Sometimes a preflight HTTP request is launched by the browser. It intents to query the server about what CORS attributes are authorized and supported by it. However, this does not change the trustiness relationship between server and browser.
The preflight HTTP request (which takes the form of an HTTP OPTIONS request) results in an equally trusted HTTP response. The only difference resides in the headers, that indicate the browser how to proceed to get the intended cross-origin resource.
As we move to the hands-on sections of this article, this will get more palatable.
CORS basic headers
Achieving origin control by the CORS involves the following headers’ family:
The headers marked with YES at the “Used for Preflight HTTP ” column play crucial preflight functions.
It goes from denoting which specific headers (Access-Control-Allow-Headers) and HTTP methods (Access-Control-Allow-Methods) are allowed, the maximum amount of seconds the browser should cache the Preflight request (Access-Control-Max-Age), request source (Origin), to the allowance (Access-Control-Allow-Credentials) of cookies to be sent by browsers in the actual request.
In the overall context, the Access-Control-Allow-Origin (ACAO) stands as the most relevant header regarding cross-origin authorization. Through this header, the server is able to tell the browser which domains it should trust.
This great power comes with significant responsibilities, though.
Allowing too much is not cool at all
It is clear to us that CORS is a useful way to extend SOP policies. However, we should take into account the impact of a full nonrestrictive rule. Let’s take the following statement:
- Access-Control-Allow-Origin: *
The above header means that every origin could access the desired resource. This would be equivalent to the earlier configuration of older browsers before SOP was in place. But be aware of the other side of the story, as cautiously depicted by the application security literature.
“Obviously there might be cases where a wildcard value for the Access-Control-Allow-Origin isn’t insecure. For instance, if a permissive policy is only used to provide content that doesn’t contain sensitive information.”
The Browser Hacker’s Handbook 2nd Ed. — Chapter 4 Bypassing the Same Origin Policy
You should be careful with the wildcards, though. Indeed, the recommended approach is make the access permission explicit for the authorized origins. If the server does not provide the CORS headers whatsoever, the browsers will assume the Same Origin Policy (SOP) posture.
The Docker-based proposed scenario
First, lets clone the `hacking-cors` repository so we can get this party started!
- $ git clone firstname.lastname@example.org:lvrosa/hacking-cors.git
The above structure breaks down the docker containers and files that compose the server images.
- $ cd hacking-cors; npm install
We have two distinct docker projects. They are represented by their respective configuration Dockerfile at the root directory.
The `img` directory from the Trusted Site stores the `owasp_bug.png` resource. In our experiment, this image resource will be requested by the Evil Site, which will then try to load it. The same applies to the `static/hello_script.js` file, but to execute/evaluate the script content, and not load an image.
Docker images and network settings overview
Take a look at the `docker-compose.yml` file above. We can identify important things about the environment like:
- We’ll create two containers (namely `evil_site` and `trusted_site`)
- The containers are attached to the `cors_hack_net` bridged network interface
- The `cors_hack_net` interface determines the subnet by the CIDR 10.5.0.0/16
As a useful link to the containers’ network addresses, I’d recommend setting your static lookup table for hostnames file (`/etc/hosts`) to the following settings:
A glance at the Apache Server CORS rules
★ Identify the Trusted Site container’s name:
★ Log into the container:
- $ docker exec -it trusted_site /bin/bash
★ Dump the Apache `.htaccess` configuration file to the screen:
Note that the `.htaccess` file is sourced at `htdocs` directory, thus impacting all the files under its substructure.
Running the containerized environment
To get these environment live, follow the steps:
★ Build the container from the root project directory
- $ docker-compose build — no-cache; (note the double minus before the no-cache argument)
★ Up the containers through `docker-compose`
- $ docker-compose up
If you run into any trouble with the docker environment, I suggest you clean it by killing and removing the current active docker images. Just ensure you don’t have other containers that should have their state saved before being killed.
- $ docker kill $(docker ps -a -q)
- $ docker rm $(docker ps -a -q)
Playing with CORS rules using XHR and Fetch requests
CORS rules for XHR requests
The XMLHttpRequest Standard defines an API that provides scripted client functionality for transferring data between a client and a server.
Regarding its security aspects, we should be aware of some warning facts, as stated by the Browser Security Handbook, part2.
“The set of security-relevant features provided by XMLHttpRequest, and not seen in other browser mechanisms, is as follows:
- The ability to specify an arbitrary HTTP request method (via the open() method),
- The ability to set custom HTTP headers on a request (via setRequestHeader()),
- The ability to read back full response headers (via getResponseHeader() and getAllResponseHeaders()),
Since all requests sent via XMLHttpRequest include a browser-maintained set of cookies for the target site and given that the mechanism provides a far greater ability to interact with server-side components than any other feature available to scripts, it is extremely important to build in proper security controls.”
It’s essential to observe the following browser protections against origin tampering upon handling XHR requests.
- When trying to change/poison the `origin` header via setRequestHeader(), the original header is preserved.
- When trying to change/poison the additional headers (passed as arguments to the xhr.send()), the original header is preserved.
CORS rules for Fetch requests
Although The Fetch Standard supersedes the `origin` element semantics (originally defined in The Web Origin Concept — RFC6454), the same effect of the XHR tampering protections applies here.
- When trying to change/poison the `origin` header via fetch() arguments, the original header is preserved.
Understanding the requests cycle in practice
Now that you already know the fundamentals of SOP, CORS, XHR and Fetch requests, we’re ready to play with the proposed scenario.
Both Trusted Site and Evil Site pages have similar menus. Firstly, try the four requests options of the Trusted Site.
Note that the requested resources, the OWASP bug image or the script that displays “All your base belong to us ;D”, are successfully loaded and consumed by the page. This is a classical case of local resources invoked by one page belonging to the same domain.
Now, let’s change the scope to the Evil Site.
- Open the `http://evilsite.com` website
- Bring up the Web Developer Tools window (press F12 in Firefox or Chrome)
Get your focus on the Console tab from Web Developer Tools. We should be looking at something like the screen below.
The third and fourth steps generate the messages from the screen above. They are transcripted below for accessibility purposes.
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://trustedsite.com/img/owasp_bug.png. (Reason: CORS header ‘Access-Control-Allow-Origin’ does not match ‘http://trustedsite.com’).
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://trustedsite.com/static/hello_script.js. (Reason: CORS header ‘Access-Control-Allow-Origin’ does not match ‘http://trustedsite.com’).
From here, it’s possible to understand why the browser has prohibited the requested resource. Remember the Apache Server .htaccess configuration, which adds the ACAO (Access-Control-Allow-Origin) CORS header to specific HTTP responses (image files and .js scripts).
Now, we are going to investigate the requests in more detail. Click the Network tab from the Web Developer Tools. Then, select the OPTIONS request for the `hello_script.js` resource. On the right side of the screen, you should see the following HTTP request and response.
Take note of the `Origin` header from above. It reads `http://evilsite.com`. But if we dive into the `script_fetch.js` implementation of the Evil Site, something stands out about the `Origin` header.
Although we try to overlap the `Origin` header (line 7) with the authorized address of Trusted Site‘s domain — `http://trustedite.com`, the Fetch API implemented by the browser prevents this from taking effect.
That’s why we see `http://evilsite.com` as `Origin` in the Web Developer Tools’ Network tab — HTTP OPTIONS request.
Bypassing CORS through proxy interception (Manual)
It’s been a long road so far. So if you made it here, it’s time to put some salt in the soup.
The main principle behind the CORS (and policies like CSP — Content Security Policy) is the trust-based relationship between browser and web server. The web server instructs the web browser about which domains it can further trust. In practice, the HTTP security headers set this instruction.
Now, let’s think from a malicious perspective. To bypass the CORS rules, the attacker has to intercept the server’s HTTP response, which contains the CORS ACAO (Access-Control-Allow-Origin) header. Secondly, he/she changes its value to reflect the attacker’s page origin or to allow arbitrary domains (using the character *).
When it’s said “to intercept”, it can be a proxy server filtering the HTTP request-response cycle automatically (see the next section for more on this). Or a manual approach through a proxy tool like Burp Suite, as we will do right after.
Setting up your snoopy proxy tool
If you are already familiarized with proxy tools like ZAP, Burp Suite, feel free to move to the next section. Here we are going to use Burp Suite Community from PortSwagger.
Install the Burp Suite according to your platform/architecture and run it.
★ At the Options tab, edit the Interface column at the Proxy Listeners settings as below (“127.0.0.1:10001”):
★ At the same tab, set the Intercept Client Requests settings as following:
★ Still at the Options tab, set the Intercept Server Responses settings as following:
★ Ensure that the “Intercept is on” button from Intercept tab is active
Finally, on your browser, you will configure it to pass the requests through our proxy. Note that the proxy is listening on port 10001. Here we have two configuration options. I usually set my proxies using the FoxyProxy extension. But you can do it manually by the Network Settings from your browser.
Ok, that’s all. We’re ready to go.
Bypassing CORS by HTTP Response tampering
★ Open the Evil Site (`http://evilsite.com`)
As previously observed, resources from the Trusted Site requested by the Evil Site are not authorized to be consumed by the corresponding page. It results from the fact that the CORS ACAO (Access-Control-Allow-Origin) header only allows the `http://trustedsite.com` domain.
★ Activate your Burp proxy at your browser
★ Click/request OWASP image resource via XHR Request
- Stop and look to the Burp Suite dialog respective to the HTTP Request
★ Proceed with the request without further edition by clicking in “Forward”
★ Stop at the HTTP Response which contains the CORS headers
★ Edit the CORS ACAO header value to `*`
★ Submit the response to the browser by clicking in “Forward”
★ The protected resource (OWASP bug image) content is displayed (note the absence of the CORS error message):
Since the sky’s the limit, feel free to try the bypassing against the remaining options from the Evil Site menu.
Besides changing the Access-Control-Allow-Origin header, you will also have to add the Access-Control-Allow-Headers in the HTTP OPTIONS request (use the Add button from the Burp request edit dialog).
This is necessary because the client (the .js script which launches the XHR request) adds the headers `cache-control` and `pragma` in the subsequent GET request by default. So you will want to reflect this in the HTTP OPTIONS response.
Remember, here we have a HTTP Preflight scenario (review the Preflight HTTP Request section when in doubts), where a HTTP OPTIONS request precedes the actual resource retrieval request.
Automatic bypassing and other CORS interesting projects
In the previous section, we saw how to bypass the CORS rules protection manually. However, this is not very efficient from a practical standpoint.
One feasible way to automate the bypassing process is by deploying a proxy server like CORS Anywhere API. The proxy server will act as an intermediary, filtering the request and response headers to reflect the allow and deny rules specified at proxy configuration time.
If you’ve got this far, congratulations. HTTP Headers by itself is a very vast topic, as the protocol tries to evolve to get close to web applications reality. This reality becomes more and more pervasive, especially with the popularization of APIs through technologies like REST.
The set of CORS headers are intricate and full of nuances. When implementing a solution that deals with inter-domain communication, pay attention to the common pitfalls that can arise. The article from `moesif.com`’s blog does a great job explaining this (and more).
The final message about CORS and its weaknesses is that the trust between browser and application should be explicitly guaranteed by secure configurations. The right choice for which AJAX method and CORS headers to deploy will positively impact your APIs’ overall security.
Remember our four proposed resource consumption scenarios. They are simple. Indeed, there are other options and combinations for resource sharing. But maybe (and I hope we got there) the theory and practice exposed here can be taken as a foundation to design more complex and secure by-default web applications.
- Mozilla Firefox 68.5.0esr (64-bit)
- Google Chrome Version 70.0.3538.77 (Official Build) (64-bit)
- Google Chrome (2018 old version) 70
Special thanks to reviewers Luiz Rennó and Vitoria Rio.