Intro

CORS stands for Cross Origin Resource Sharing. As the name suggests, it is a mechanism that enables fetching of resources from different origin. At first, this concept by now is so natural that I didn’t think it needed any further configuration other than just doing it! But it wasn’t. Let us delve into what it is and how it works! For this posting I read the official document from [https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS]

1. Main mechanism

img

  • The web document shown in the device is from domain-a.com
  • The blue image in the square is from the same domain server, domain-a.com.
  • The red canvas w/image in the square is from different domain, domain-b.com.
  • CORS mechanism

2. Scenario

Overview

  • CORS standard works by adding a new HTTP headers that let servers describe which origins are permitted to read that information from a web server. Since HTTP requests can cause side-effects on server data(GET, POST, PUT with MIME types), the specification mandates that browsers “preflight” the request. Preflight specifies, with HTTP OPTION method, which methods from the server is supported. After obtaining the server’s approval, browsers can send the actual request for resources. Servers can inform clients whether credentials should be sent with requests.

CORS failure specifics are NOT available for Javascript for security reasons. The only way to determine what went wrong is to look at the browser’s console for details.

2-1. Simple requests

  • Some requests don’t trigger CORS preflight. They should meet the below criteria.

One of the allowed methods: GET, HEAD, POST Headers defined in “CORS-safelisted request-header”: Accpet, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width Values allowed for Content-type: application/x-www-form-urlencoded, multipart/form-data, text/plain

  • Let’s look at some concrete example below. img
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
  • This is the request that client sends, note that the invocation is coming from “https://foo.example”.
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
  • This is the response header from the server. Note that it sends Access-Control-Allow-Origin header. It simply says that the resource can be accessed by any domain. If the server wants to allow it only for foo.example,
Access-Control-Allow-Origin: https://foo.example
  • This was the simple request.

2-2. Preflighted request

  • Unlike simple requests, preflighted requests first send an HTTP request by the OPTION method to the resource on the other domain, to determine if the actual request is safe to send. They are used when headers specifed other than mentioned in “simple requests” are present, to prevent modifications in user data.
    const xhr = new XMLHttpRequest();
    xhr.open('POST', 'https://bar.other/resources/post-here/');
    xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
    xhr.setRequestHeader('Content-Type', 'application/xml');
    xhr.onreadystatechange = handler;
    xhr.send('<person><name>Arun</name></person>');

It creates an XML body to send with the POST request. A non-standard HTTP X-PINGOTHER not presentt in “simple requests”

img

  • Below is the preflight request/response (highted red in the diagram above)
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type


HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
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
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
  • In the response header above, you can note Access-Control-Allow-Headers: X-PINGOTHER, Content-Type, which tells the browser that PINGOTHER is allowed. It is import to allow OPTION method to be available for all origins at least, so that this preflight request can be made from the client. After this handshake is made, the real request (marked blue) can be sent.
POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache

<person><name>Arun</name></person>


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
  • Not all browsers support following redirects after a preflighted request. If a redirect occurs, some browsers may report an error such as

    The request was redirected to ‘https://example.com/foo’, which is disallowed for cross-origin requests that require preflight Request requires preflight, which is disallowed to follow cross-origin redirect

Until some of those browsers catch up with the implementation, you may apply the following workaround.

  • Change the server-side behavior to avoid the preflight and/or to avoid the redirect
  • Change the request such that it is a simple request that doesn’t cause a preflight If that’s not possible, then another way is to:
  • Make a simple request (using Response.url for the Fetch API, or XMLHttpRequest.responseURL) to determine what URL the real preflighted request would end up at.
  • Make another request (the “real” request) using the URL you obtained from Response.url or XMLHttpRequest.responseURL in the first step.

2-3. Request with credentials

Both XMLHttpRequest/Fetch and CORS can make “credentialed” requests that are aware of HTTP cookies and HTTP authentiation information. By default, browsers will NOT send credentials. But a specific flag can be set on request for it.

    const invocation = new XMLHttpRequest();
    const url = 'http://bar.other/resources/credentialed-content/';
        
    function callOtherDomain() {
    if (invocation) {
        invocation.open('GET', url, true);
        invocation.withCredentials = true;
        invocation.onreadystatechange = handler;
        invocation.send(); 
    }
    }

img

Below is the sample exchange between client and server.

GET /resources/credentialed-content/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Referer: http://foo.example/examples/credential.html
Origin: http://foo.example
Cookie: pageAccess=2


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
  • Cookie: pageAccess=2 may be allowed, but if the host bar.other did not respond with Access-Control-Allow-Credentials: true, the response would be ignored and not be available to web content.