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/POSTand Server does not respond withAccess-Control-Allow-Originmatching or wildcard* - Client sends simple
GET/POSTwith credentials and Server does not responde withAccess-Control-Allow-Originas an exact match ANDAccess-Control-Allow-Credentials: true - Client sends a
PUT/DELETEand Server does not respond withAccess-Control-Allow-Originmatching or wildcard*ANDAccess-Control-Allow-Methodsdoes not include the request method - Client sends a
PUT/DELETEwith credentials and Server does not respond withAccess-Control-Allow-Originmatching or wildcard*ANDAccess-Control-Allow-Methodsdoes not include the request method ANDAccess-Control-Allow-Credentialsis 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-Originunsent:"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.comCross-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.comCross-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.comSuccessful
Access-Control-Allow-Origin: http://cors-client.jordansavant.com/fooCross-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.comCross-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.comthat isclient=abc - There is a cookie underneath
.jordansavant.comthat isroot=def - The server set a cookie on each request
cors-server.jordansavant.comthat 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.comCross-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.comandAccess-Control-Allow-Credentials: trueSuccessful
Visible Server Cookies:
rootandserver
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-Originunsent: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, POSTSuccessful
Access-Control-Allow-Origin: http://cors-client.jordansavant.comCross-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.comandAccess-Control-Allow-Methods: GET, PUT, POSTSuccessful
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.comandAccess-Control-Allow-Methods: GET, PUT, POSTCross-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.comandAccess-Control-Allow-Methods: GET, PUT, POSTandAccess-Control-Allow-Credentials: trueSuccessful
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.