For clarity, I have included an example implemented in popular Django framework. Here you can download it and try it out. But let’s tackle these issues one at a time. Since the space for the article is limited, I will focus on giving you a general idea and an example. You can find comprehensive descriptions on sekurak (unfortunately it’s in Polish), W3 consortium documentation or Mozilla documentation. I strongly advise you to go through them before writing your own implementation. The aim of this article is to show how it works and provide you with a ready app you could experiment with.
What is CORS?
What we have to know for the article purpose is that in order to provide web apps environments isolation, browsers do not allow different origins to communicate unless there was explicit permission. What is the origin? According to RFC-6454 it’s a set of values: schema, host, and port unequivocally defining given web application, where the port can be given implicitly (eg. for http:// schema it will be port 80).
- a request method is other than GET, HEAD, POST, or
- headers are set by hand to something outside the following scope: Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width, or
- Content-Type has different value than application/x-www-form-urlencoded, multipart/form-data, text/plain, or
- an object of ReadableStrem type has been used in a request, or
- we are listening for events on object XMLHttpRequestUpload.
We are dealing with so-called “Not that simple requests”. They work a little bit differently since the first browser sends a preflight request to check whether it can perform the main request. I am not dealing with this case in this article.
Simple requests work in the following way:
- A user wants to load something on origin A, which requires access to data from origin B.
- A browser sends the request to the origin B adding the Origin header in which it places the origin A.
- A server processes the request and sends back header Access-Control-Allow-Origin, with origins it trusts. It can be a specific origin (eg. http://example.com/), list of origins, trust to everyone marked by setting value “*”, null or no header. It’s worth mentioning that null can lead to non-trivial problems I will mention later. A good practice is not using the last option unless we know what we are doing.
OK, but what is the benefit?
After reading the simplified description we should notice that CORS does not give us:
- Protection against CSRF attacks, since the request is processed. If we want to protect ourselves against unauthorized POST requests, we should look at different parts of our project. By the way, the application I linked is vulnerable to this kind of attacks. I’d suggest playing around with it to test it out.
- Protection against reading data by the client (since it’s not Cross Client Resource Sharing) since the browser gets data, it only does not allow the application running in our browser to access it. Pulling out this content is, however, trivial.
CORS protects us from reading data by a foreign server. So if we were writing an application processing sensitive data (eg. a bank application) and wanted to serve frontend and backend from different endpoints and wanted to allow frontend to read the data, we would have to whitelist it. At this point, if the frontend was vulnerable to XSS attacks through including a remote script that could execute on the page, it would be impossible to get data from backend and pass it further to a foreign server controlled by an attacker.
In our example the following apps will be used:
- Django_app - will be serving as service providing data and listening on http://localhost:8000/api/v1/albums/,
- Flask_app - will be serving as the frontend for showing data available on http://localhost:5000/.
According to origin definition, we split infrastructure to 2 elements, which have to communicate with each other. In src/django_app/settings.py we define a list of origins to whom we grant reading permissions:
CORS_ORIGIN_WHITELIST = ( 'http://localhost:5000', )
We run both applications and view data (important: we go to http://localhost:5000/ not http://127.0.0.1:5000/ these are 2 different origins):
Now we can comment out the allowed origin from backend and repeat the action. Origin http://localhost:5000/ will be acting as a malicious site that tries to gather sensitive data (because who would like to confess to listening to Coma?) from an unaware user. This time nothing will appear apart from the following error:
(index):1 Access to XMLHttpRequest at 'http://localhost:8000/api/v1/albums/' from origin 'http://localhost:5000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
What can go wrong?
As I’ve mentioned, CORS does not give protection against CSRF-class attacks. Additionally, if we wanted to share data to all hosts in a subdomain, we could try to use a regular expression, eg. allow origins matching pattern: http:\/\/(.)*example.com. The attacker could register http://trololoexample.com, which would match.
If we wanted to share data only to one domain, but on any port, we could use the following regex: http:\/\/\.example.com.*, but this could be bypassed by registering trololo.net with subdomain http://example.com.trolololo.net, that would make a successful match.
We might use a library with invalid implementation. The one I used for Django until recently allowed to use HTTP when we might require HTTPS making MITM attacks easier.
Apart from that, we might not want to share data and set header Access-Controll-Allow-Origin: null, but the browser will add “Origin: null” when we make a request from pseudoscheme like file://. This will make origins match and allow for reading data.
The list of potential problems does not end here. I hope I managed to make you aware of how many things could go wrong and convince that implementation details should be consulted with a specialist.