How Does Setting Up CORS Help Prevent Cyber Attacks?
Cross Origin Resource Sharing (CORS) is key to making websites work the way we want them to. How does it protect us from cyber attacks?
The short answer is IT doesn’t.
Have you ever seen this error?
Developers usually follow this up with a google search like “disable CORS chrome.” They often do this during development because CORS gets in their way. There are also several misconceptions about how CORS is related to various types of cyber attacks.
To clear things up, CORS by itself does not prevent or protect against any cyber attack. It does not stop cross-site scripting (XSS) attacks. It actually opens up a door that is closed by a security measure called the same-origin policy (SOP).
The same-origin policy is a concept implemented by web browsers that prevent one web page from accessing sensitive data on another page. This type of attack is called a cross-site request forgery (CSRF or XSRF). Here are the basics:
- You log into a website that you trust (i.e., your bank). You authenticate using your username, password, and maybe 2FA. The session is stored as a cookie in your browser.
- When you load other pages on the bank website or take actions on your account (e.g., transfer money), the browser uses an AJAX request to access a REST endpoint to retrieve private data or make changes to your account.
- You open a malicious website in another browser tab. This site is designed to send AJAX requests to your bank’s REST API endpoint (this usually happens in the background without you even knowing). The browser sends the request with the bank cookie from your valid session.
There are many variations on this type of attack and lots of details around GET vs. POST, pre-flight checks, etc. In the general case, SOP would prevent the malicious website from being able to do anything with the bank’s REST endpoint. When the malicious site sends the AJAX request to the endpoint, the browser checks that the origin doing the requesting (the malicious site) matches the origin where the rest is sent (the bank). When these don’t match, javascript code on the malicious site is prevented from accessing the response.
—--
Now, let’s change the scenario. In step 1 above, the bank website is www.bank.com, and the REST endpoint the bank uses is api.bank.com. SOP treats these as different origins. This bank website would not work because SOP would prevent the bank website from accessing the REST endpoint.
So, how do we allow www.bank.com to access api.bank.com, while blocking everyone else? Enter CORS.
We can tell browsers which cross-site requests are safe using CORS. In this scenario, we add the CORS HTTP headers to the api.bank.com endpoint that will tell the browser:
When a page at www.bank.com tries to send me an AJAX request, allow it. If it’s anyone else, block it.
This is accomplished using the Access-Control-Allow-Origin
header. Every response from api.bank.com should include this header:
Access-Control-Allow-Origin: https://www.bank.com
Now we have used CORS to open the door that SOP closes, but only for our trusted domain. Notice that CORS headers are applied to the REST endpoint, not the original bank page. As developers, we often add the header with a wildcard just to get our app working. This configuration allows access to your REST endpoint from ANY origin. Depending on what your app does, this could be very bad if used in production.
//don’t do this in production unless you know what you are doing
Access-Control-Allow-Origin: *
—--
Same-Origin Policy Allows Malicious Sites to Send Credentialed Requests
SOP Enforcement does NOT prevent a malicious site from sending requests to the REST endpoint with the real credentials stored in your browser as a cookie. It prevents the page from reading the response. This is an interesting nuance. CSRF attacks run malicious code in the user’s web browser. If allowed to execute, this malicious code could perform unintended actions on behalf of the user on the target website (i.e., the bank above) or send the user’s session information to the attacker. The attacker could then use those session credentials to log in as the user and do whatever they want.
Why would the server send the request when it knows that the origins don’t match?
It does this because cross-site requests are quite common and make the web usable, efficient, and fast for us. In fact, for certain types of requests and when REST semantics are implemented correctly, there is no security concern (well, specifically related to cross-origin security - there is never a situation where there is NO security concern). Images, fonts, CSS, etc., can be loaded cross-origin without issue.
CORS Simple and Preflighted Requests
Simple requests exist only because this is how things were done before CORS was a thing. The CORS specification has a very detailed definition of what types of requests qualify as simple. GET and POST (under certain conditions) are considered ‘simple.’
GET requests are used when there should be no danger in sending the request as-is. If the API is designed correctly, GETs should never change state on the server. They should be idempotent (i.e., you can send them once or multiple times without changing the outcome). In contrast, a POST or PUT request is supposed to change state on the server and therefore should only be sent once. If you kept sending POST requests that transferred money, you could overdraw your account!
GET requests are safe for the browser to send immediately. Upon receipt, the server checks that the origin is allowed (and checks your credentials) in the request and sends the response with the Access-Control-Allow-Origin
header set. If the header and page origin do not match, the browser blocks the response from the requesting page.
So, can’t an attacker create a request to your REST endpoint with whatever Origin and Host header they want?
YES, they can. Anyone can use browsers or other tools (e.g., curl) to format an HTTP request and send it to your endpoint. An attacker can set the Origin to match a legitimate one (i.e., set the Origin header to https://www.bank.com and send a request to http://api.bank.com to try and do something nefarious. These are not successful because they do not have your credentials. Remember CSRF attacks only work because the attacker needs your browser to send your cookies with the request to api.bank.com.
For requests that do not qualify as ‘simple,’ the CORS spec requires a pre-flight. This is an extra handshake between the browser and the server using the HTTP OPTIONS method to determine if the actual request is cross-origin compatible. This means the browser will not send the real POST or PUT request if the pre-flight fails.
This example shows how the pre-flight check protects the user in the scenario described above. When the user has logged into their bank's website and visited the bad guy site in a different tab, the CSRF attack is possible. Here we see that the browser sends the bad guy's request to api.bank.com, but it fails because the origin (badguy.com) does not match the Access-Control-Allow-Origin
header returned by the bank. Note that CORS uses some other headers like Access-Control-Allow-Headers
and Access-Control-Max-Age
, but I left them off the digrams for simplicity.
Ideally, pre-flight would occur on every cross-origin request, but it does take extra time, and there are legacy systems still active that would not be compatible. Whether the browser uses pre-flight or not, the server must always check whether each request received is cross-origin allowable and check the user’s credentials before changing or returning any data.
Thanks for reading! If you enjoyed this content, please also check me out on Medium: https://medium.com/@ezrabowman