- Pre-Requisites
- Why do we need CORS?
- Introduction
- Basics
- 4.1. Request Headers
- 4.1.1. Origin
- 4.1.2. Access-Control-Request-Method
- 4.1.3. Access-Control-Request-Headers
- 4.1.4. CORS-safelisted
- 4.2. Response Headers
- 4.2.1. Access-Control-Allow-Origin
- 4.2.2. Access-Control-Allow-Methods
- 4.2.3. Access-Control-Allow-Headers
- 4.2.4. Access-Control-Max-Age
- 4.2.5. Vary
- 4.3. Simple Requests
- 4.4. Preflight Requests
- 4.5. Request with credentials
- 4.1. Request Headers
- Advanced Topics
- TODO
- References
Know difference between Simple HTTP and XHR request.
Origin Web App - https://github.com
When you open a website by typing Origin Web App, your browser makes Simple HTTP requests. CORS is not applicable here. As we are voluntarily requesting to serve content from Origin Web App. Your browser will/should serve it without any trouble. But, when it comes to XHR requests, JavaScript which is loaded by Origin Web App in your browser can make a request to any server(https://netflix.com) requesting for a resource. Now, Origin Web App is owned by Github but https://netflix.com is owned by Netflix, that server could pontentially serve anything. Github cannot take control of a server which is owned by Netflix. This has lot of security implications pontentially stealing a content from one website(it could be a financial website) to any remote server.
Luckily, CORS tackles this problem very well with a given set of rules.
Its a standard defined by W3C to enable cross-origin requests between client(browser) and the server to share resources at the same time maintaining security. Any browser will comply with these standards to prevent loading resources from any third-party servers.
A header indicates where a request originates from. It is sent with CORS requests, as well as with POST requests.
Syntax:
Origin: <scheme> "://" <hostname> [":" <port> ]
Examples:
Origin: https://netflix.com
Origin: http://netflix.com:443
Origin: http://localhost:1443
This header is used by browser when issuing a pre-flight requests to indicate which request method will be used when the actual request is made.
Syntax:
Access-Control-Request-Method: <method>
<method>
could be either GET
, POST
or DELETE
.
Example:
Access-Control-Request-Method: POST
This header is again used in the pre-flight requests by browser to indicate which request headers are to be used when the actual request is made. To use multiple headers, it has to be separated with comma.
Syntax:
Access-Control-Request-Headers: <header-name>
Access-Control-Request-Headers: <header-name>, <header-name>
Example:
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Important: All the header should be CORS-safelisted or a custom header like X-Custom-Header
.
-
Accept
-
Accept-Language
-
Content-Language
-
Content-Type
Allowed values are
application/x-www-form-urlencoded
,multipart/form-data
andtext/plain
.
The following headers are returned in the pre-flight requests.
This header indicates that if the requested Origin is allowed. Your browser will choose to succeed/fail the request by matching the requested origin with this.
Syntax:
Access-Control-Allow-Origin: *
For requests without credentials, the value *
can be specified as a wildcard.
This tells your browser to allow requests from any Origin.
Access-Control-Allow-Origin: <origin>
When you receive only one origin in the response header, it means your server/web-app
based on the requested Origin
it responds with same Origin
if its allowed.
Your server should also respond with Vary to indicate that
it varies based on a request header.
Example:
Access-Control-Allow-Origin: https://github.com
Your browser will make actual request if one of the value in this header matches.
When wildcard *
is returned, it means any method is allowed.
Syntax:
Access-Control-Allow-Methods: <method>, <method>, ...
Access-Control-Allow-Methods: *
Example:
Access-Control-Allow-Methods: GET, POST
You browser will make actual request if all of the requested headers are allowed.
Syntax:
Access-Control-Allow-Headers: <header-name>, <header-name>
Access-Control-Allow-Headers: *
Example:
Access-Control-Allow-Headers: Accept, Content-Type
Wildcard *
tells browser that any header is allowed.
Tells how long the results of pre-flight can be cached.
Access-Control-Max-Age: <delta-seconds>
maximum no. of seconds the results can be cached.
Each browser has max limit,
- Firefox caps this at 24 hours(86400 seconds).
- Chromium (prior to v76) caps at 10 minutes (600 seconds).
- Chromium (starting in v76) caps at 2 hours (7200 seconds).
- Chromium also specifies a default value of 5 seconds.
- A value of -1 will disable caching, requiring a preflight OPTIONS check for all calls.
Vary header in general used for caching purpose to determine whether a cached response can be used or it has to be re-cached based on the header value.
In CORS, lets say server allows multiple origins based on the requested Origin
it will
return particular URL in Access-Control-Allow-Origin
.
Syntax:
Vary: <header-name>
Example:
Lets say github.com allows both https://github.com as well as https://netflix.com to request for resources. Consider following scenarios,
Scenario 1:
curl -X OPTIONS https://github.com/api/v1/gists/1
Origin: https://github.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: Content-Type
Access-Control-Allow-Origin: https://github.com
Vary: Origin
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 600
Now in this scenario, browser will cache this pre-flight request results for 10 minutes.
Scenario 2:
curl -X OPTIONS https://github.com/api/v1/gists/1
Origin: https://netflix.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: Content-Type
Access-Control-Allow-Origin: https://netflix.com
Vary: Origin
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 300
Now you could notice that Access-Control-Allow-Origin
contains https://netflix.com,
this is example of how the response varies based on given Origin
. So does the
max-age of this response which is cached for only 5 minutes unlike first scenario.
Some request don't trigger pre-flight request for CORS. These we call it as simple requests. It should meet the following conditions:
-
One of the allowed methods:
- GET
- POST
- DELETE
-
Other than headers which are automatically by the user agent (for example,
Connection
orUser-Agent
), the only headers which are allowed to manually set are CORS-safelisted request headers and the following:DPR
Downlink
Save-Data
Viewport-Width
Width
-
No event listeners are registered on any
XMLHttpRequestUpload
object used in the request. -
No
ReadableStream
object is used in the request.
Example:
Request:
GET /api/v1/public-data/ HTTP/1.1
Host: bar.com
User-Agent: curl/7.69.1
Accept: */*
Origin: https://foo.com
Response Headers:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
When a request returns *
in Access-Control-Allow-Origin
header. It means a request
can be made from any Host
. In this case, your browser won't make pre-flight request.
Your browser will make pre-flight request to determine if the actual request is safe to send.
Example:
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.com/api/v1/post-here/');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
xhr.onreadystatechange = handler;
xhr.send(JSON.stringify({ "email": "[email protected]" }));
Pre-flight Request:
OPTIONS /api/v1/post-here/
Host: bar.com
User-Agent: curl/7.69.1
Accept: */*
Origin: https://foo.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Type: application/json
Access-Control-Allow-Origin: https://foo.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type, Accept
Actual Request:
POST -d '{foo: bar}' /api/v1/post-here/
Host: bar.com
User-Agent: curl/7.69.1
Accept: */*
Origin: https://foo.com
X-PINGOTHER: pingpong
Content-Type: application/json;charset=UTF-8
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Type: application/json
Access-Control-Allow-Origin: https://foo.com
When a non-standard header like X-PINGOTHER
is set, your browser won't know if its
safe to make the request. In order to make sure its safe, your browser makes OPTIONS
request with Access-Control-Request-Headers
containing X-PINGOTHER
and Content-Type
. Upon validating with response headers of pre-flight
request, your browser makes actual request.
In general, when you make a XHR request, cookies are not passed along with request.
When there is need to pass cookies, you'll have to set a flag on the XMLHttpRequest
object.
const xhr = new XMLHttpRequest();
const url = 'http://bar.com/api/v1/credentialed-content/';
function callOtherDomain() {
if (invocation) {
xhr.open('GET', url, true);
xhr.withCredentials = true;
xhr.onreadystatechange = handler;
xhr.send();
}
}
Request:
POST -d '{foo: bar}' /api/v1/post-here/
Host: bar.com
User-Agent: curl/7.69.1
Accept: */*
Origin: https://foo.com
X-PINGOTHER: pingpong
Content-Type: application/json;charset=UTF-8
Cookie: _session=NyV14oKXiS6HHopaf9nT
When a request is made XMLHttpRequest
and withCredentials
flag is set, your
browser will pass down the Cookie
in the request header.
Response:
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Type: application/json
Access-Control-Allow-Origin: https://foo.com
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: _session=AjBSqxj4T7bSySNTWeEm; expires=Wed, 31-05-2020 00:00:00 GMT
When your browser notices Access-Control-Allow-Credentials
set to true. It will
respect Set-Cookie
header and sets the cookie.
Important: "*" wildcard should not be set in the Access-Control-Allow-Origin
like
mentioned in the Credentialed requests and wildcards section.
When a credentials request is made by setting withCredentials
flag, Access-Control-Expose-Headers
has to be set by server to let browser know which headers can be accessed.
In pre-cors world, by default response headers are not accessible by browser in the CORS request. So it is made explicit so that browser will look for this header in order to read exposed headers. This way CORS specification makes sure old browsers doesn't break.
This is returned in the pre-flight requests. When your browser
sees this, it can access Set-Cookie
header. As we mentioned above, in normal XHR requests, your
browser won't pass Cookie
in request header as well as read Set-Cookie
response header.
Syntax:
Access-Control-Allow-Credentials: true
You can find example in the Request with credentials section.
When we say credentialed request it means cookies passed in the XHR request or set via the response
header Set-Cookie
. When a XHR request is made with withCredentials flag, you're hoping to receive
response along with cookies to be set. But, you cannot expect Access-Control-Allow-Origin
to be "*"
because that would mean any website can use these cookies. For this reason, your browser would fail
the request if it sees "*" in Access-Control-Allow-Origin
response header.
When a pre-flight request responds with 301/302, some browsers may not support this currently. You might get errors like,
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
Note: For workarounds check Preflighted requests and redirects docs by Mozilla.
Browser has settings to reject all third-party
cookies, when a user enables that. For example, if a request is
made from https://foo.com
and server is at https://bar.com
, your browser will not set cookies sent by https://bar.com
.
- Add Screencasts