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 withAccess-Control-Allow-Origin
matching or wildcard*
- Client sends simple
GET
/POST
with credentials and Server does not responde withAccess-Control-Allow-Origin
as an exact match ANDAccess-Control-Allow-Credentials: true
- Client sends a
PUT
/DELETE
and Server does not respond withAccess-Control-Allow-Origin
matching or wildcard*
ANDAccess-Control-Allow-Methods
does not include the request method - Client sends a
PUT
/DELETE
with credentials and Server does not respond withAccess-Control-Allow-Origin
matching or wildcard*
ANDAccess-Control-Allow-Methods
does not include the request method ANDAccess-Control-Allow-Credentials
is nottrue
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 isclient=abc
- There is a cookie underneath
.jordansavant.com
that isroot=def
- The server set a cookie on each request
cors-server.jordansavant.com
that isserver=<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
andAccess-Control-Allow-Credentials: true
Successful
Visible Server Cookies:
root
andserver
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: *
andAccess-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
andAccess-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
andAccess-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
andAccess-Control-Allow-Methods: GET, PUT, POST
andAccess-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.