Hacking HTTP CORS from inside out: a theory to practice approach

Lucas Vinícius da Rosa
InfoSec Write-ups
Published in
16 min readOct 14, 2020

--

Foreword

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 ;
  • How CORS rules apply to different JavaScript API implementations (XHR and Fetch);
  • 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[1].

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`:

Domain types differentiation

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 [2].

SOP variations

There are some different types of Same Origin Policy. In practice, they are applied under particular rules given the technology in place.

The primary SOP policy developed was towards the DOM (Document Object Model) access. It is a mechanism that handles JavaScript and other scripting languages’ ability to access DOM properties and methods across domains.

The DOM access is based mainly on the `document.domain` DOM’s property state. But the same is not valid for the JavaScript’s XMLHttpRequest API, which does not take `document.domain` property into account. For now, don’t worry too much about these details. We should be back to this subject soon.

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[3]. 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.

Simple Request

In contrast to Preflight requests, there are simple ones. Simple requests are those who respect the following conditions:

Simple requests composition

When making asynchronous HTTP requests, we often use the already presented XMLHttpRequest[5] or the new Fetch[6] JavaScript APIs.

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.

Pre-request flight flow for deletion of avatar.org resource from api.domain.org

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:

CORS headers family and their respective HTTP type

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`[7] repository so we can get this party started!

  • $ git clone git@github.com:lvrosa/hacking-cors.git
Tree directory representation of the hacking-cors docker project

The above structure breaks down the docker containers and files that compose the server images.

The `node_modules` directory (omitted from the `tree` command output above) is pushed to the `hacking-cors` repository as well. It has a short size. However, if you have any problems regarding JavaScript dependencies, please run the `npm`[8] tool at the project root directory.

  • $ cd hacking-cors; npm install

It is also crucial to have `docker`[9] and `docker-compose`[10] installed on your system. The docker-compose file uses version 3 format. My current versions for both dependencies are:

Docker and Docker Compose versions

Project structure

We have two distinct docker projects. They are represented by their respective configuration Dockerfile at the root directory.

Docker files respective to each web server

Interesting files are at the `static` and `img` directories. They hold the resources that will eventually be solicited by other web pages. JavaScript files are under the `static` directory. You can open them in your favorite code editor and play around.

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

docker-compose.yml content

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:

/etc/hosts file mapping

A glance at the Apache Server CORS rules

★ Identify the Trusted Site container’s name:

docker-compose ps output

★ Log into the container:

  • $ docker exec -it trusted_site /bin/bash

★ Dump the Apache `.htaccess` configuration file to the screen:

.htaccess Apache CORS extension rules

As highlighted in the picture above, the Apache Web Server will serve the ACAO (Access-Control-Allow-Origin) header when the file-matching rule applies. In other words, if the web browser requests an image (.gif, .png, .ico, etc.) or even a JavaScript .js file, the resource’s content will only be authorized to be loaded/consumed by the page if the origin matches `http://trustedsite.com`.

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
docker-compose up output

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)

Well, I hope everything has been ok so far. Let’s take a look at the two created web servers. Open your browser and drive to the `http://trustedsite.com` and `http://evilsite.com`.

Trusted Site

Trusted Site Web Server menu

Evil Site

Evil Site Web Server Menu

Playing with CORS rules using XHR and Fetch requests

CORS rules for XHR requests

The XMLHttpRequest Standard[11] 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[12].

“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()),

The ability to read back the full response body as a JavaScript string (via responseText property).

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

Numerous APIs provide the ability to fetch a resource, e.g. HTML’s img and script element, CSS’ cursor and list-style-image, the navigator.sendBeacon() and self.importScripts() JavaScript APIs. The Fetch Standard provides a unified architecture for these features so they are all consistent when it comes to various aspects of fetching, such as redirects and the CORS protocol.” — https://fetch.spec.whatwg.org/

Although The Fetch Standard supersedes the `origin` element semantics (originally defined in The Web Origin Concept[13]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.

The hello_script.js’ content executed after one same domain page request

Now, let’s change the scope to the Evil Site.

  1. Open the `http://evilsite.com` website
  2. Bring up the Web Developer Tools window (press F12 in Firefox or Chrome)
  3. Click the anchor link associated with the OWASP image + JavaScript XHR
  4. Click the anchor link associated with the Alert script + JavaScript Fetch

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.

OPTIONS HTTP request and response for hello_script.js resource

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.

`evil_site_public_html/static/script_fetch.js` content

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[14] 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[15]. But you can do it manually by the Network Settings[16] from your browser.

My Firefox’s FoxyProxy burp proxy settings

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

  1. 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):

Hacking homework

In the earlier scenario, we intercepted and tampered with one JavaScript XHR request via Burp proxy. One important aspect of this outside the box approach is to analyze the HTTP request-response cycle thoroughly. Which headers are demanded and what error messages you got at your browser console when they are missing or misconfigured.

Now it’s time to let you apply by yourself the knowledge exposed ‘till here. The next section will also present some hints about the CORS bypassing for the Alert script + JavaScript FETCH combination.

Since the sky’s the limit, feel free to try the bypassing against the remaining options from the Evil Site menu.

Tips for bypassing the Alert script + JavaScript FETCH

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.

HTTP Options response edited to reflect the CORS headers for bypassing
HTTP GET response from `hello_script.js` resource retrieval (note the ACAO CORS header)
Successful CORS hacking get us the nice dialog above

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[17] 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.

Please, refer to the project’s Github page[18] for more details and precautions when setting a proxy facing the web (be careful with open proxies, friend). You can test the live API here[19].

Another fantastic initiative around the CORS (and CSP — Content Security Policy) header’s understanding is the `CORS Demos`[20] from `digi.ninja`[21]. Very nice proof of concept.

Concluding remarks

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[22] 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.

Used browsers

  • 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.

--

--

Security Engineer, Ethical Hacker (CEH Master) and Independent (Portuguese) Literature Author