Tighten up Your CORS Policy

E.Y.
6 min readJan 19, 2021
Photo by amirali mirhashemian on Unsplash

Not long ago, my friend’s company (let’s say amazingCorp conducted a Pen test against the whole codebase and identified a few security vulnerabilities. One of them being a loose CORS policy which allows domains ending with amazingCorp.com (e.g. mostamazingCorp.com) to make cross-domain requests to API.amazingCorp.com.

This can be leveraged to steal information about users. As a result, some user coercion is required and there are other mitigating factors detailed in this report.

Below is a ticket my friend created for this problem:

Given I am on a non amazingCorp domain e.g mostamazingCorp.com
When I am making an http request to api.amazingCorp.com
Then I should not be able to complete the request
Given I am on an invalid amazingCorp subdomain domain e.g most.amazingCorp.com
When I am making an http request to api.amazingCorp.com
Then I should not be able to complete the request
Given I am on a valid amazingCorp subdomain domain e.g another.amazingCorp.com
When I am making an http request to api.amazingCorp.com
Then I should be able to complete the request

But before we jump into the problem — what is CORS first?

(Some of the information below quoted and adapted from MDN)

The Cross-Origin Resource Sharing (CORS) standard works by adding new HTTP headers which allow servers to serve resources to permitted origin domains. Browsers support these headers and respect the restrictions they establish.

For security reasons, browsers forbids cross-origin HTTP requests initiated from scripts. (e.g. XMLHttpRequest and the Fetch API follow the same-origin policy).

This cross-origin sharing standard can enable cross-site HTTP requests for:

  • Invocations of the XMLHttpRequest or Fetch APIs, as discussed above.
  • Web Fonts (for cross-domain font usage in @font-face within CSS).
  • WebGL textures.
  • Images/video frames drawn to a canvas using drawImage().
  • CSS Shapes from images.

The requests can be split into three categories below:

1. Simple Request

  • One of the allowed methods: GET / HEAD / POST
  • Only have headers automatically set by the user agent, or the headers include:Accept / Accept-Language/ Content-Language /Content-Type (with additional requirements below)
  • The only allowed certain values for the Content-Type header application/x-www-form-urlencoded or multipart/form-data or text/plain
  • No event listeners are registered on any XMLHttpRequest.upload object used in the request
  • No ReadableStream object is used in the request.

For example, let’s sayhttps://foo.example wants to request something from bar.example :

GET /resources/public-data/ HTTP/1.1
Host: bar.other
Origin: https://foo.example
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *

2. Preflight request

Anything not a Simple request should be a Preflight request. For example, foo.example` wants to send a POST request to bar.example with a non-standard HTTP X-PINGOTHER request header. Since the request uses a Content-Type of application/xml, and since a custom header is set, this request is preflighted.

//Preflight-request 
OPTIONS
/resources/post-here/ HTTP/1.1
Host: bar.example
...
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
//Server response
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
//Real request
POST /doc HTTP/1.1
Host: bar.example
....
X-PINGOTHER: pingpong
Referer: https://foo.example/examples/preflightInvocation.html
Origin: https://foo.example

<person><name>Arun</name></person>
//Server response
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://foo.example

[Some XML payload]

3. Request with Credentials

We can also make request that contain HTTP cookies and Authentication information.

In this example, content originally loaded from http://foo.example makes a simple GET request to a resource on http://bar.example which sets Cookies. To do that, we need to set withCredentials onXMLHttpRequest to true .

Also, the server must specify Access-Control-Allow-Credentials: true header and set a specific origin in the value of the Access-Control-Allow-Origin header, instead of "*" wildcard. Otherwise, the browser will reject any response.

GET /resources/credentialed-content/ HTTP/1.1
Host: bar.example
Origin: http://foo.example
Cookie: pageAccess=2
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
[text/plain payload]

CORS Request Headers

Origin: indicates the origin of the cross-site access request or preflight request. In any access control request, it is mandatory, as well as for all requests whose method is neither `GET` nor `HEAD`.

Origin: <origin> //The origin value can be null, or a Uri

Access-Control-Request-Method: indicates which method a future CORS request to the same resource might use.

Access-Control-Request-Headers: indicates which headers a future CORS request to the same resource might use.

Access-Control-Request-Headers: <field-name>[, <field-name>]*

CORS Response Headers

Access-Control-Allow-Origin: specifies either a single origin, or for requests without credentials , using the “*" wildcard to allow any origin to access the resource.

 Access-Control-Allow-Origin: <origin> | *1. resource can be accessed by any origin:Access-Control-Allow-Origin: *2. only from foo.example
Access-Control-Allow-Origin: https://foo.example

Access-Control-Expose-Headers: whitelists headers that Javascript in browsers are allowed to access.

Access-Control-Expose-Headers: <header-name>[, <header-name>]*

Access-Control-Max-Age: indicates how long the results of a preflight request can be cached.

Access-Control-Max-Age: <delta-seconds>

Access-Control-Allow-Credentials: indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials.

Access-Control-Allow-Credentials: true

Access-Control-Allow-Methods: specifies methods allowed when accessing the resource. This is used in response to a preflight request.

Access-Control-Allow-Methods: <method>[, <method>]*

Access-Control-Allow-Headers: responds to a preflight request to indicate which HTTP headers can be used when making the actual request. This is the server side response to the browser's Access-Control-Request-Headers header.

Access-Control-Allow-Headers: <header-name>[, <header-name>]*

Back to the Problem

So now we understand the mechanism of CORS, how do we tackle the above problem?

First let’s look at how the current set up works (server using an express framework)

//app.js
const express = require("express");
const cors = require("cors");
const app = express();const corsOptions = {
origin: /amazingCorp\.com(:\d+)?$/,
credentials: true
};
app.use(cors(corsOptions));

Now, looking at the ticket requirements, since we want to only allow specific domains and they are different in staging vs prod, introducing config files will be a good approach.

config/default.json{
"corsAllowedOrigins":
[
"^http(s)?:\\/\\/dev\\.amazingCorp\\.com(:\\d+)?$",
"^http(s)?:\\/\\/localhost(:\\d+)?$" ]
}
config/staging.json{
"corsAllowedOrigins":
[
"^http(s)?:\\/\\/staging\\.amazingCorp\\.com(:\\d+)?$",
"^http(s)?:\\/\\/valid\\.amazingCorp\\.com(:\\d+)?$"
}
config/prod.json{
"corsAllowedOrigins":
[
"^http(s)?:\\/\\/www\\.amazingCorp\\.com(:\\d+)?$",
"^http(s)?:\\/\\/amazingCorp\\.com(:\\d+)?$",
"^http(s)?:\\/\\/valid\\.amazingCorp\\.com(:\\d+)?$"
}
//app.js
const express = require("express");
const cors = require("cors");
const app = express();const corsAllowedOrigins = config.get("corsAllowedOrigins").map(
allowedDomainRegex => new RegExp(allowedDomainRegex))
const corsOptions = {
origin: corsAllowedOrigins,
credentials: true
};
app.use(cors(corsOptions));

That’s it! Just using some regex pattern to make sure you exclude all malicious/ambiguous domain while whitelisting the necessary ones.

That’s so much of it! Happy Reading!

P.S. Also refer to glossary below for CORS related terms:

Options Header

The HTTP OPTIONS method requests permitted communication options for a given URL or server. It is an HTTP/1.1 method that is used to determine further information from servers, and is a safe method, meaning that it can’t be used to change the resource.

OPTIONS /index.html HTTP/1.1
OPTIONS * HTTP/1.1

MIME

A media type (also known as a Multipurpose Internet Mail Extensions or MIME type) is a standard that indicates the nature and format of a document, file, or assortment of bytes. It is defined and standardized in IETF’s RFC 6838. e.g. application/javascript

Accept vs. Content-Type

The Content-Type entity header is used to indicate the media type of the resource. The Accept header field can be used by user agents to specify response media types that are acceptable.

Accept header is used by HTTP clients to tell the server which type of content they expect as response. Content-type can be used both by clients and servers to identify the types of the data in their request (client) or response (server) so the other party can interpret the information correctly. For example, with post or put you have the payload in request, so have to use content-type header.

Vary Header

The Vary response header determines how to match future request headers to decide whether a cached response can be used.

Vary: *
Vary: <header-name>, <header-name>, ...
  • * : Each request for a URL is supposed to be treated as a unique and uncacheable request.
  • <header-name>: A comma-separated list of header names to take into account when deciding whether or not a cached response can be used.

--

--