CORS! Understanding the Common Issues.
CORS is one of the most common issues with APIs in general.
Browsers have a security feature called Same-Origin
Policy. This policy prevents a web page from making requests to a
different domain than the one that served the web page.
This is a security feature to prevent Cross-Site Request Forgery (CSRF) attacks.
TLDR, CSRF is an attack where a malicious website sends a request to a different website on behalf of the user.
Basically, Forging a request and sending it on behalf of the user to the server. (It's more prevelant than you think).
To protect against CSRF attacks, the browser implements the Same-Origin
policy.
Origin
is defined as the combination of protocol
, host
, and port
.
If the Origin
of the request and the Origin
of the response are the same, the browser allows the request.
For example,
Same Origin? | http://example.com/foo | http://example.com/bar | https://example.com/foo | https://www.example.com/foo |
---|---|---|---|---|
http://example.com/foo | Yes (Same protocol, host and port) | Yes (Same protocol, host and port) | No. Protocol is different | No. Protocol and Host are different |
http://example.com/bar | Yes (Same protocol, host and port) | Yes (Same protocol, host and port) | No. Protocol is different | No. Protocol and Host are different |
https://example.com/foo | No. Protocol is different. | No. Protocol is different | Yes (Same protocol, host and port) | No. Host is different |
https://www.example.com/foo | No. Protocol and Host are different | No. Protocol and Host are different | No. Host is different | Yes (Same protocol, host and port) |
Now suppose the frontend has to make a request to the backend at https://api.example.com/
and frontend is hosted
at https://www.example.com/
. When you invoke the fetch
API of the browser, here is what happens:
- The browser before sending the actual request to the server, sends a
preflight
request to the server. - A preflight request is an HTTP OPTIONS request (OPTIONS method is just like other HTTP requests like GET, POST, PUT). But it is sent to the server primarily to ask if the server actually allows the original request to be sent.
An OPTIONS request has the following headers:
OPTIONS /profile/post/ HTTP/1.1
Host: https://api.example.com
...
Origin: https://www.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
The OPTIONS
request asks the server if it allows the POST
request from https://www.example.com
with the header Content-Type
.
The server responds with the following headers:
HTTP/1.1 200 OK
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
...
Here, in the preflight response, the server says that it allows the POST
(along with GET
and OPTIONS
) request from https://www.example.com
with the header Content-Type
.
If the server does not respond with the Access-Control-Allow-Origin
header,
the browser will not send the actual request to the server and you will see the following error in the console:
Access to fetch at 'https://api.example.com/profile/post' from origin 'https://www.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Therefore, the server has to respond with the Access-Control-Allow-Origin
header for the OPTIONS
request to allow the actual request
to be made to the server.
If you are Javascript developer, you might have used the cors package in Node.js to enable CORS. This library abstracts
out a lot of these OPTIONS
request handling for you. While abstraction is good, but using this library without understanding
CORS can lead to a lot of confusion.
Quick Note on expressjs-cors middleware
The cors
middleware in expressjs is used to enable CORS in the server. It is used generally as follows:
...
const corsOptions = {
origin: ,
credentials: ,
}
app.use(cors(corsOptions));
...
- The origin parameter is used to specify the origin from which the server allows requests. If you set it to
*
, it allows requests from all origins (There is gotcha though). You can also set it to a specific origin likehttps://www.example.com
or to a regular expression to match multiple origins or to an array of allowed origins or totrue
which basically sets theAccess-Control-Allow-Origin
header to theOrigin
of the request (virtually same as setting it to "*"). - The credentials parameter is used to specify if the server allows credentials to be sent with the request. If you set it to
true
, the server will respond with theAccess-Control-Allow-Credentials
header set totrue
. Credentials are cookies, TLS client certificates, or authentication headers containing a username and password. By default, these credentials are not sent in cross-origin requests, and doing so can make a site vulnerable to CSRF attacks.
** The gotcha with setting the origin
to *
is that you cannot set the Access-Control-Allow-Credentials
header to true
.
If you do, the browser will not allow the request. Therefore, if you have to send credentials with the request,
you have to set the origin
to a specific origin and set the credentials
to `true.
If you do want to allow requests from all origins and also send credentials, you have to set the Access-Control-Allow-Origin
header to the Origin
of the request in the server.
This is simply done by setting the origin
to true
in the cors
middleware.