Create a Mental Model of Cross-Origin Resource Sharing (CORS)
Vincent Duarte8 min read
Create your own Cross-Origin Resource Sharing (CORS) visualization and test it on an interactive CORS testing site.
Discover the CORS policy, a security feature implemented by browsers to protect you from phishing attacks. Learn about Access-Control-Allow-Origin
and Access-Control-Allow-Methods
headers, and the purpose of preflight requests.
🧠 What is Cross-Origin Resource Sharing?
CORS - Cross-Origin Resource Sharing - defines a security protocol that browsers apply.
Browsers act as intermediaries between websites and servers, and they can stop HTTP requests if the server does not whitelist the website or the requested method.
They protect users against hacking attacks such as phishing and cross-site request forgery (CSRF). Additionally, they can also mitigate cross-site scripting (XSS) attacks.
For example:
A user is connected to their bank, https://my-bank.com
, on their browser. They then visit another - malicious - website, http://malicious-website.com
, without knowing the danger.
The malicious website could send requests via a script to the bank’s API to steal data. This theft is possible when the browser retains the user’s bank session through cookies and is configured to send cookies automatically.
However, if the bank server does not specifically allow-list http://malicious-website.com
, the browser will hide the server’s response from the malicious website’s script.
With the CORS policy, the browser will compare the request origin (the website’s URL) with the URLs allowed by the server. If the origins do not match, the browser stops the process.
🔎 When am I concerned by CORS?
Are you developing a website with a remote API? Are your website and API accessible on different URLs?
If so, then you are concerned by CORS 😁.
You may have already experienced the infamous “CORS error” in your developer tools’ “Network” tab.
The next sections will explain what happens between your front end and your remote server.
🕹️ CORS Playground: Try it yourself
Here is a little project to play with Cross-Origin Resource Sharing:
🔗 https://cors-training-website.vercel.app
🔎 On this site, you can send requests to a remote API using different configurations. It provides a straightforward way to apply and visualize CORS theories in practice. This interactive app is designed for hands-on learning and experimentation.
Keep this mini-project handy as you read this article 🙂.
You can access the GitHub repository for the CORS training website by clicking here.
🧑🏫 CORS: Example and explanation
Let’s imagine we are developing a website with a public remote API. Our website will be at https://my-site.com
and our API will be at https://my-api.com
. Our objectives are to send GET and DELETE HTTP requests. However, even though our remote API is public, we want to ensure only we can access our data.
Let’s start with the GET request.
Compare URLs: Allow-Control-Allow-Origin and Origin
To protect our data, we need to whitelist only our website https://my-site.com
on our API. This way, even if we later open a malicious website that sends a GET request via a script, our browser will hide all server responses from its script.
Thanks to this strategy, the malicious website won’t be able to retrieve the data we have on the server.
Now, let’s look at how we can define an origin. Consider this URL:
https://my-site.com:3000
There are 3 important components here:
- The protocol: HTTPS
- The domain: my-site.com
- The port: 3000
With these 3 components, you can now perfectly define your origin. Let’s try a guess:
Question: Which of these URLs matches the origin:
https://my-site.com:3000
?
-
http://my-site.com:3000
❌ The protocol HTTP does not match the protocol HTTPS.
-
https://subdomain.my-site.com:3000
❌ Subdomains do not match the origin.
-
https://my-site.com
❌ The port must be indicated.
If no port is expected in the allowed origin, the origin should not indicate a port either.
-
https://my-site.com:3000/home?page=2
✅ Path and parameters do not affect the matching with the origin.
Here is an example of a failing request due to mismatching origins:
💡 Try it yourself on my side project at https://cors-training-website.vercel.app.
Starting from section 5 to the end, and also at section 4 in part.
If we check the “Response” tab in Firefox’s DevTools, let’s see what we get:
Firefox is a funny case because it allows you, as a user, to see the server’s answer. Firefox’s DevTools might show the response, but scripts on the malicious site will not be able to access the response due to CORS protections.
The purpose of a script on a malicious website is to steal data. Cross-Origin Resource Sharing policy protects you by stopping the process. However, it is okay for you, as a victim, to see your own data.
If we use a REST client like Postman to call the route, we could easily read the response because Postman is not a browser and thus has no Cross-Origin Resource Sharing protection.
This is fine because a hacker would find it very difficult to trick a victim into consciously sending a request with their login information and returning the response.
Let’s take Chrome as an example to see unreadable responses in case of CORS errors:
Let’s put what happened with a GET request by schemas.
Example: GET request refused
Example: GET request successful
⚠️ Be careful not to modify your data with a GET request. Your server will execute your request!
That’s it for the GET request. However, how do we protect against other methods like DELETE?
Let’s move on to the DELETE request.
Preflight Request: Checking if the HTTP Request can be sent
You may have noticed that some HTTP requests, such as DELETE, send two calls to your server.
Illustration: Preflight Request in Chrome Browser DevTools
Illustration: Preflight Request in Firefox Browser DevTools
This is a consequence of the CORS principle. Let’s dive deeper into what this means and how it works.
The server will execute a request.
Without additional security, all requests sent to a server will be executed. In our example, if http://malicious-website.com
sends a DELETE HTTP request to our server, the script does not need read the response. The script has achieved its goal as long as the backend processes the request and removes a resource.
Cross-Origin Resource Sharing protection prevents sending the request to the server. Before sending your DELETE HTTP request, your browser will ask the server if it can. This is a preflight request.
The preflight request uses the HTTP OPTIONS method on the same path and adds the header Access-Control-Request-Method
to indicate the method it intends to use. In our example, it is the DELETE method. The server will respond with a list of all authorized methods on this route in the response header Access-Control-Allow-Methods
. If DELETE is in this header, the browser will send the original request ; otherwise, it won’t.
Example of a Preflight Failure:
Example of a Preflight Success:
💡 If the origins do not match in the preflight request, the main request will not be sent.
⚠️⚠️⚠️ Warning :
There is no preflight request for the methods GET, HEAD, and POST(with standard headers). So be careful not to modify your data in a route called by one of these methods. The POST method should be used to add data, and this could also pose a problem.
🕹️ Try yourself on my side project https://cors-training-website.vercel.app
That’s it 🙂. We now have a solid understanding of the CORS principle 🥳.
🚀 To Go Further
Great! We now know how to whitelist our website on our remote API!
“But what if I am developing a public API? Open for everyone?”
There are different ways to define an origin. I invite you to read the ReadMe from ExpressJS’s CORS repo.
However, you can allow any website to call your API by setting your Access-Control-Allow-Origin
to "*"
.
⚠️ Nevertheless, be aware that being open to everyone also means being open to anyone, even malicious websites.
I also encourage you to read articles about the CORS header Access-Control-Allow-Credentials
. This adds security for authenticated requests, i.e., requests with cookies, TLS client certificates, or authentication headers.
- What is Access-Control-Allow-Credentials?
- Why the combination of allow credentials at true and allow origin * does not work
🏁 Conclusion
Takeaways:
- CORS stands for Cross-Origin Resource Sharing.
- CORS is a security mechanism applied by browsers.
- CORS checks the validity of the request origin.
- CORS checks the validity of the requested method.
- Next step: Access-Control-Allow-Credentials.
Here’s a complete diagram of a valid DELETE request to help you visualize how it all works.
There are many ways to define the origin
in the context of CORS. The most important step is to clearly define your needs. Is it a public API or a private website?
Many articles on the internet will advise you to set origin
to *
and enable all methods by default. Most of the time, this is a bad practice. Considering security from the start can help protect you from vulnerabilities.
The best practice is to set up a whitelist to maintain control over your resources.
Additional security tips include not returning the allowed origin when a request fails to avoid giving too much information to an attacker. Also, you can define CORS properties in your whole API, or at each route to be more specific for each need.
Thank you for reading my article on CORS.
Your comments and feedback are greatly appreciated!
Don’t forget to star my GitHub project and download it to explore CORS for yourself: GitHub Repository
Happy experimenting with CORS!