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 requestGiven 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 requestGiven 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
headerapplication/x-www-form-urlencoded
ormultipart/form-data
ortext/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.exampleHTTP/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=2HTTP/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.