Jordan Savant // Software Engineer

CORS

Cross Origin Resource Sharing is a way to protect a user's browser session from being exploited by rogue js to make calls on their behalf to other services.

For example, if I had logged into my bank at bank.com my browser would be allowed to make requests to the bank's api to receive information, make transactions etc. This is all allowed operations while on bank.com, but we need to be secured from the same calls made from google.com. If a rogue js had inflitrated google.com and CORS was not in place, it could make requests to bank.com which would appear valid because I had logged in and a session cookie existed in my browser. Information would be stolen, transactions made, all against my will.

With CORS in place, thoses requests can still issue and the responses return, but the browser will prevent the js from accessing the response. All requests by default must match: domain, protocol, and port.

There are two types of requests: simple and preflight.

A simple request is your standard GET, POST etc with only simple Content-type values of application/x-ww-form-urlencoded, multipart/form-data, text-plain.

Requests that send more information such as cookies (and therefore are of a different Content-type) a preflight mechanism kicks in where an OPTIONS request is made before the official request to check for CORS access before sending the simple request.

Client Side

Origin

This header comes in the request from the client and contains the domain of the issuer. It is set by the browser and cannot be overwritten for security reasons.

Access-Control-Request-Method

Sent in preflight requests to check if the server supports the HTTP method. Access-Control-Request-Method: PUT

Access-Control-Request-Headers

Sent in a preflight request to let the server know what headers it could expect in the followup request: Access-Control-Request-Headers: <field-name>[, <field-name>]*

Server Side

Access-Control-Allow-Origin

Access-Control-Allow-Origin is a header sent back from the server in the response.

The value can be * to allow any domain to access or a fully qualified domain name https://www.example.com to limit the access.

In the case that you require the client to send cookies or other Authentication headers, the Access-Control-Allow-Origin: * response will not be supported.

Access-Control-Allow-Headers

Used in response to preflight OPTIONS call.

A comma separated list of request header values the server supports. Useful if you also use custom headers.

Access-Control-Expose-Headers

Used in response to preflight OPTIONS call.

A comma separated list of headers allowed to be exposed to the client on response.

Access-Control-Allow-Methods

Used in response to preflight OPTIONS call.

A comma separated list of HTTP request type such as GET, POST which the server supports.

Access-Control-Allow-Credentials

Access-Control-Allow-Credentials is required if credentials (cookies) are sent in the request.

It must be true or false as a value.

Dealing with Cookies / Authentication

By default browsers will not send cookies and authentication headers to the server in a request. In Javascript we can tell the XMLHttpRequest object to include that credentialed information with withCredentials = true.

The request could still be a simple GET and no preflight check sent first, but all responses that do not contain Access-Control-Allow-Credentials: true will be isolated by the browser. In addition the server must also respond with Access-Control-Allow-Origin and a specific origin and not *.

Securing CORS in Server Side

Ultimately the CORS security can be bypassed with proxy servers and non-browser requests, so it is primarily a browser security element to protect end users who interact with the server through browser scripts. So CORS is not a reliable to limit your server from unwanted requests, but it is a way to protect consumers from being exploited.

It will fail if:

  • Client sends simple GET/POST and Server does not respond with Access-Control-Allow-Origin matching or wildcard *
  • Client sends simple GET/POST with credentials and Server does not responde with Access-Control-Allow-Origin as an exact match AND Access-Control-Allow-Credentials: true
  • Client sends a PUT/DELETE and Server does not respond with Access-Control-Allow-Origin matching or wildcard * AND Access-Control-Allow-Methods does not include the request method
  • Client sends a PUT/DELETE with credentials and Server does not respond with Access-Control-Allow-Origin matching or wildcard * AND Access-Control-Allow-Methods does not include the request method AND Access-Control-Allow-Credentials is not true

Here is some testing scenarios and how the CORS security reacted:

Simple Requests / No Credentials

Client: cors-client.jordansavant.com

Server: cors-server.jordansavant.com

var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == XMLHttpRequest.DONE) {   // XMLHttpRequest.DONE == 4
        document.getElementById("response").innerHTML = xmlhttp.responseText || 'fail';
        document.getElementById("status").innerHTML = xmlhttp.status || 'fail';
    }
};
xmlhttp.open("GET", "http://cors-server.jordansavant.com", true);
xmlhttp.send();

A GET request with headers is sent that looks like:

Accept              */*
Accept-Encoding     gzip, deflate
Accept-Language     en-US,en;q=0.5
Connection          keep-alive
DNT                 1
Host                cors-server.jordansavant.com
Origin              http://cors-client.jordansavant.com
Referer             http://cors-client.jordansavant.com/
User-Agent          Mozilla/5.0 (Windows NT 10.0; ...) Gecko/20100101 Firefox/58.0
  • Access-Control-Allow-Origin unsent:

    "Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://cors-server.jordansavant.com/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)."

  • Access-Control-Allow-Origin: *

    Successful

  • Access-Control-Allow-Origin: http://praetor64.com

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://cors-server.jordansavant.com/. (Reason: CORS header ‘Access-Control-Allow-Origin’ does not match ‘http://praetor64.com’).

  • Access-Control-Allow-Origin: http://cors-server.jordansavant.com

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://cors-server.jordansavant.com/. (Reason: CORS header ‘Access-Control-Allow-Origin’ does not match ‘http://cors-server.jordansavant.com’).

  • Access-Control-Allow-Origin: http://cors-client.jordansavant.com

    Successful

  • Access-Control-Allow-Origin: http://cors-client.jordansavant.com/foo

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://cors-server.jordansavant.com/. (Reason: CORS header ‘Access-Control-Allow-Origin’ does not match ‘http://cors-client.jordansavant.com/foo’).

  • Access-Control-Allow-Origin: http://cors-client.Jordansavant.com

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://cors-server.jordansavant.com/. (Reason: CORS header ‘Access-Control-Allow-Origin’ does not match ‘http://cors-client.Jordansavant.com’).

Simple Requests / With Credentials

Existing Cookies:

  • There is a cookie underneath cors-client.jordansavant.com that is client=abc
  • There is a cookie underneath .jordansavant.com that is root=def
  • The server set a cookie on each request cors-server.jordansavant.com that is server=<random string>
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == XMLHttpRequest.DONE) {   // XMLHttpRequest.DONE == 4
        document.getElementById("response").innerHTML = xmlhttp.responseText || 'fail';
        document.getElementById("status").innerHTML = xmlhttp.status || 'fail';
    }
};
xmlhttp.open("GET", "http://cors-server.jordansavant.com", true);
xmlhttp.withCredentials = true;
xmlhttp.send();

A GET request with headers is sent that looks like:

Accept              */*
Accept-Encoding     gzip, deflate
Accept-Language     en-US,en;q=0.5
Connection          keep-alive
Cookie              _root=def; server=hij
DNT                 1
Host                cors-server.jordansavant.com
Origin              http://cors-client.jordansavant.com
Referer             http://cors-client.jordansavant.com/
User-Agent          Mozilla/5.0 (Windows NT 10.0; ...) Gecko/20100101 Firefox/58.0
  • Access-Control-Allow-Origin: *

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ‘http://cors-server.jordansavant.com/?r=ncreds-all’. (Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’).

  • Access-Control-Allow-Origin: http://cors-client.jordansavant.com

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://cors-server.jordansavant.com/?r=creds. (Reason: expected ‘true’ in CORS header ‘Access-Control-Allow-Credentials’).

  • Access-Control-Allow-Origin: http://cors-client.jordansavant.com and Access-Control-Allow-Credentials: true

    Successful

    Visible Server Cookies: root and server

The cookies stored underneath the client, however, are not sent / visible.

Preflight Requests / No Credentials

var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == XMLHttpRequest.DONE) {   // XMLHttpRequest.DONE == 4
        document.getElementById("response").innerHTML = xmlhttp.responseText || 'fail';
        document.getElementById("status").innerHTML = xmlhttp.status || 'fail';
    }
};
xmlhttp.open("PUT", "http://cors-server.jordansavant.com", true);
xmlhttp.withCredentials = true;
xmlhttp.send();

A preflight OPTIONS request is made that looks like:

Accept                          text/html,application/xhtml+xm…plication/xml;q=0.9,*/*;q=0.8
Accept-Encoding                 zip, deflate
Accept-Language                 en-US,en;q=0.5
Access-Control-Request-Method   PUT
Connection                      keep-alive
DNT                             1
Host                            cors-server.jordansavant.com
Origin                          http://cors-client.jordansavant.com
User-Agent                      Mozilla/5.0 (Windows NT 10.0; ...) Gecko/20100101 Firefox/58.0

And on success a PUT request is made that looks like:

Accept              */*
Accept-Encoding     gzip, deflate
Accept-Language     en-US,en;q=0.5
Connection          keep-alive
Content-Length      0
DNT                 1
Host                cors-server.jordansavant.com
Origin              http://cors-client.jordansavant.com
Referer             http://cors-client.jordansavant.com/
User-Agent          Mozilla/5.0 (Windows NT 10.0; ...) Gecko/20100101 Firefox/58.0
  • Access-Control-Allow-Origin unsent:

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://cors-server.jordansavant.com/?r=put-none. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

  • Access-Control-Allow-Origin: *

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://cors-server.jordansavant.com/?r=put-all. (Reason: Did not find method in CORS header ‘Access-Control-Allow-Methods’).

  • Access-Control-Allow-Origin: * and Access-Control-Allow-Methods: GET, PUT, POST

    Successful

  • Access-Control-Allow-Origin: http://cors-client.jordansavant.com

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://cors-server.jordansavant.com/?r=put-match. (Reason: Did not find method in CORS header ‘Access-Control-Allow-Methods’).

  • Access-Control-Allow-Origin: http://cors-client.jordansavant.com and Access-Control-Allow-Methods: GET, PUT, POST

    Successful

In the final scenario the request is allowed because the Origin explicitly matches and the PUT is in the allowed methods.

Preflight Requests / With Credentials

  • Access-Control-Allow-Origin: http://cors-client.jordansavant.com and Access-Control-Allow-Methods: GET, PUT, POST

    Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://cors-server.jordansavant.com/?r=put-match-allow-nocreds. (Reason: expected ‘true’ in CORS header ‘Access-Control-Allow-Credentials’).

  • Access-Control-Allow-Origin: http://cors-client.jordansavant.com and Access-Control-Allow-Methods: GET, PUT, POST and Access-Control-Allow-Credentials: true

    Successful

Spoofing Proof with CURL

The above scenarios show how to configure the browser client code and the server side response code to work through CORS security protocols. But we must realize that the CORS security measures are browser specific and do not enforce and level of security outside of that.

Here are some examples of the same calls being made with CURL and how the response is always recieved no matter what.

$ curl -X PUT -H "Cookie: root=abc;server=123" http://cors-server.jordansavant.com?r=put-match-allow-creds -v
> PUT /?r=put-match-allow-creds HTTP/1.1
> Host: cors-server.jordansavant.com
> User-Agent: curl/7.47.0
> Accept: */*
> Cookie: root=abc;server=123
>
< HTTP/1.1 200 OK
< Date: Sun, 11 Mar 2018 15:32:43 GMT
< Server: Apache/2.4.18 (Ubuntu)
< Access-Control-Allow-Origin: http://cors-client.jordansavant.com
< Access-Control-Allow-Methods: GET, PUT, POST
< Access-Control-Allow-Credentials: true
< Content-Length: 176
< Content-Type: application/json
<
{
    "response": true,
    "method": "PUT",
    "request": {
        "r": "put-match-allow-creds"
    },
    "cookie": {
        "root": "abc",
        "server": "123"
    }
}

So if a simple CURL request can spoof the Origin and Cookie headers, then how is CORS a security measure at all?

If our server set cookies as a way to identify the session of a user in the browser, it would set them under the server's domain. Any rogue js loaded into the browser from another domain would only have access to the domain of the presentation page and not cookies set in the domain of the server. This is a browser security restriction to prevent session hijacking through XSS attacks.

Given this, even though a person could spoof the id being sent through the Cookie header, they cannot actually steal a known cookie and make those same requests.

When to Use This

If you design an API that will be using user authentication or session identification to send requests back and forth, it is an important security layer. By using Cookies to identify a user, the browser will secure it from rogue js taking it as it will be stored under your API's domain and not under the domain of the website that makes the request.

Therefore it is important that any public endpoints that manipulate sensitive user information be secured with cookie identification.