CORS, or Cross-Origin Resource Sharing, is vital to learn in web security. Everyone in my classes seemed to struggle with this topic throughout our two semesters of API development. I want to learn how to more effectively handle CORS issues. Therefore, I decided I want to try to explain my WEB-IR topics by also creating projects. I need to log 75 hours into this IR class, so that will take up more time. For this project, we will be making simple API requests from my shared server to a Heroku server.
What is CORS?
According to the W3 documentation:
[This] includes advice for specifications that define APIs that use the cross-origin request algorithm defined in this specification — CORS API specifications — and the general security considerations section includes some advice for client-side Web application authors.
So, that’s cool and all. But what does it mean? Put simply, Cross-Origin Resource Sharing is a set of standards for two domains to interact. To protect data from cross-domain request hacks, CORS acts as an authorization barrier.
The Same-Origin Policy Issue
The general standard for web requests is to only accept scripts from the same “origin”. This origin consists of:
- the URI scheme
- the host name
- the port number
Unless otherwise specified, servers block unmatched requests. This protects our information as a default precaution. So, what’s the problem? I don’t want a random, malicious website to make requests to my bank account. Well, yeah, I don’t want that either. But I do want https://maryn.xyz/kitten-adoptions to make requests to a server I’m hosting on https://maryns-midterm.herokuapp.com.
So, that’s where CORS works its magic. One must manually enable Cross-Origin Resource Sharing in order to bypass the Same-Origin Policy. This means you must must write everything intentionally. Test absolutely everything. It only takes one slip-up to allow for compromise, especially if you are making your API public!
Security Flaws Without SOP
On the other hand, if you don’t have the Same-Origin Policy, you expose your application to potential dangers. The most popular attack is CSRF, or “Cross-Site Request Forgery”. Browsers automatically attach cookies to your browsers’ HTTP requests, and does not require users to handle them.
The CSRF exploit takes advantage of this, by, for example, sending a POST request from a malicious site to a secure link, pretending it is coming from the user through the use of cookie authentication. I won’t get too much into it (maybe I will in another blog post). Hopefully you can see that the same-origin policy helps to protect against CSRF and other ambient authority or XSS attacks.
Why Use CORS With Your API
I feel like anyone reading this has already dealt with APIs before. If you haven’t, that’s okay! APIs are basically how two services can talk to each other. Here is a nice, long list of open APIs if you’re interested. As we have discussed earlier, cross-origin resource sharing is necessary in order for these to work.
Why use CORS with your API? Doesn’t that defeat the purpose of the same-origin policy? The
'Allow-Access-To' header (which will be discussed shortly) can be thought of as a whitelist of who can make requests. For open APIs, you can use a wildcard
* to indicate that anyone can use your API. The most common practice for sites with open APIs is to create a second barrier to entry. For example, Twitter’s API requires that you have an account, and that you take care of your public/private keys.
“I’m not using an API.” Ok there, buddy. You might also need to use CORS to get access to cross-origin fonts, images, videos, and stylesheets. It’s important, okay?
Most (if not all) of the documentation I’ve read on CORS begins by teaching you about simple requests. However, I thought it would be more beneficial to throw you into the deep end for this blog post.
A pre-flight asks the second domain if the request is safe. This is done by default through the
OPTIONS method, if the request contains (or will affect) sensitive data. Put simply, these are the PUT, DELETE, PATCH methods of the CRUD operations.
The request must contain a valid
Access-Control-Request-Method or a valid
Access-Control-Request-Header header. Then, the server will set the necessary response headers. Here’s a neat flowchart of the process (source):
content-type is going to be either
text/plain. This means that if you want to parse JSON data, the request needs to send a preflight.
Example: Pokédex Preflight Request & Response
In October 2017, I wrote a simple Pokedex thing as a part of my web app I class. I believe this was my first introduction to writing PUT and DELETE functions, so I think this is the best example to go over for a pre-flight request.
Let’s walk through the server.py code and see how CORS comes into play here.
First, on line 44, you can see that I have overwritten the
do_OPTIONS method. I have also separated the headers out into a separate function, called
end_headers (back up at line 11) to keep my code fairly DRY. You can take a look at the headers I attach there, as well as the status code I sent on line 45.
This code is not live. Let’s pretend it is. I forgot how much of a pain it is to switch from SQLite3 to Postgres for Heroku.
The client is located on my shared host, at https://maryn.xyz/pokedex. My server code, however, is hosted on Heroku, at https://maryn-pokedex.herokuapp.com. You can look through the git repository to see the how I’ve separated the code.
Now, we can obviously see that https://maryn.xyz/pokedex and https://maryn-pokedex.herokuapp.com are very different domains. The same-origin policy automatically blocks all requests that maryn makes to maryn-pokedex. We have overwritten this with the
do_OPTIONS method. Now, maryn sends a pre-flight request to maryn-pokedex for validation. In my server, I have allowed for all incoming origins, using the asterisks wildcard. Finally, the server accepts and processes the request.
If the request doesn’t send a preflight, it is a simple CORS request. For example, your GET, HEAD, and POST methods. Your simple request will also not use a ReadableStream object, or send anything other than the aforementioned
multipart/form-data. These requests are generally considered safe, and do not require much more authentication than the
Origin header, which is accepted or declined by the server.
Example: Message Log Simple Request & Response
In October, before I wrote the Pokedex application, I wrote a simple message log for my Web App class. I think it gives a great example of how CORS works with simple requests, since the only allowed requests are
POST. It was my introduction to CORS, and the beginning of my API adventures.
This application is also split between server and client, with the server hosted on https://maryn-message-log.herokuapp.com and the client being hosted on https://maryn.xyz/message-log. You can also look through the git repo to see the separated code.
This code is not live. Let’s pretend it is. I forgot how much of a pain it is to host a python web app.
Walking through the server.py code, we can first see that — god, my organisation is terribly not DRY — when a
POST request is made, the server parses the request, writes the body to my text file, and returns a 201 Created response. For the
GET requests, it simply returns whatever is in that logs.txt file, and sends it back with all the other response headers. Don’t worry, I still have yet to go over the specifics of the request and response headers.
HTTP requests send headers back and forth from client to server to hold more specific information about the requests. Key-value pairs store the data and provide additional instructions to how to handle a request. There are four categories of headers, namely general, request, response, and entity headers. Proxies might handle headers end-to-end or hop-by-hop. The mozilla documentation on headers is a fantastic resource on learning more about them.
CORS specifically deals with three main HTTP request headers.
We have talked a lot about this origin key. Its value will tell the server its URI scheme, host name, and port number. It will also determine resource access for the Origin. I probably don’t have to reiterate this, but I will, because it is the very basis of the foundation of CORS. The origin is what will allow or disallow the sharing of a cross-origin resource.
In a preflight request, this will tell the server which HTTP method the client is requesting. It is a forbidden header name, which means that only the browser can modify the value. This helps to prevent spoofing or ambient authority attacks. Its value is the method: GET, POST, DELETE, etc.
This header is also a forbidden header name. It specifies headers during the preflight request. Its value is a comma-separated list of valid HTTP headers. The one I have used most frequently is Content-Type, since I’ve mostly experienced using JSON data rather than plaintext. However, you might also use this to specify redirects, caching, cookies, and message body information. I’m pretty sure you can request any header from the previously mentioned mozilla list of headers.
In response to the requests, CORS specification will send back six HTTP response headers.
This is the server’s response to the request’s Origin header. The only two allowed directives are a wildcard, or a specific URI. According to the W3 CORS documentation, if you want to specify more than one URI, it is better practice to create a list in your server. This might be done in the
I used this header generously in my web application classes, especially when dealing with cookies and sessions. This will tell the client if the page can see the response, and if credentials (cookies, authorization headers, or TLS client certificates) can make the request. This is particularly important for preflighted requests, and works alongside the credentials sent in the request. The response will fail if the credentials are invalid in either the header or the XHR/Fetch request. The only value allowed of this header is true, since the default would be false.
By default, your response will display the six simple response headers. You can allow your response to show even more with a list of header names as the value. For example, this could be useful if I wanted to debug how to handle a response. It requires manual overriding for edge cases, and acts as a CORS precaution.
A preflight request has a default cache value of 5 seconds, but the browser sets the maximum amount of allowed cache time. For example, Firefox only allows for up to 24 hour preflight caching. More specifically, the cached information is going to be the value of your Access-Control-Allow-Methods and Access-Control-Allow-Headers headers.
This is the server’s response to the request’s Access-Control-Request-Method header in a preflight request. Therefore, the value of this header would be something like POST, GET, OPTIONS, etc. If the request’s method matches one in this comma-separated list, then it processes the response.
By default, the four simple headers are always allowed (Accept, Accept-Language, Content-Language, and Content-Type*). Everything else must be manually specified. If the request uses the Access-Control-Request-Headers header, then the response requires the Access-Control-Allow-Headers header.
* Remember, we already talked about the default
content-type as something similar to MIME type of its parsed value of form or plaintext.
CORS Example Project – the Fortune Cookie
I already have a fortune cookie project. It’s simple and an easy example to digest for the topic of this blog post. It was my first class project for Web App I, so it’s fairly basic. Perfect for being able to see the bare bones of CORS and API interaction.
If you look through the full code, you can see that I have a file called
data.JSON. This isn’t actually being used anywhere in this process, I just made it to keep track of the fortunes I want to display. Essentially, I’ve copied and pasted this information into myjson.com. If you wanted to take this a bit further, you could host this in an actual database and parse through the information yourself, as I have done in later projects.
Back to my
script.js code, on line 73, you can see my Fetch request for the data.
The CORS Request & Response
Now, when this code is executed (in this case, when the page is loaded in the browser), we can look at the network tab of the Chrome inspector to see the site’s requests and the server’s response.
In the request, the
Origin is https://maryn.xyz/fortune-cookie. In the response, the
Access-Control-Allow-Origin is a wildcard, so the request is accepted in that regard.
The data is received, and my client creates its own storage of usable fortunes.
The User Experience
When you open up the page, the request is made immediately in the background, and now my website has the data. If you click on the fortune cookie, it simply selects a random fortune from the new list.
In this example, my script executes the fetch request, but more than likely, your code will make a fetch request after the user interacts with the page. If I kept everything in a database instead of in a JSON list, I could have written my script to execute one fetch request per fortune-cookie-click to populate the fortune. There are endless possibilities to how to do this, but the main focus of using this example in this blog post is to showcase the CORS request and response.
Security with CORS
Poorly-configured CORS headers pose a huge threat to your website. For example, there was a Facebook Messenger exploit in 2016, dubbed ‘originull’. It is super important to keep in mind that you are exposing yourself when you deviate from precautions, such as the SOP. The originull exploit is actually kind of interesting, and I would love to do an in-depth analysis of that in the future.
Some Final Thoughts
Wow, thanks for reading the entire thing. Or maybe you just skimmed to the final paragraph. Regardless, I’ve had a lot of fun looking into the documentation of CORS. If you have any questions, feel free to ask me, and I will also make sure that this post is generally kept up to date!
Also, I found this cool site that will help you to test for valid CORS requests, and I will probably be using it as a resource in the future. There are other ways to relax the Single-Origin Policy, such as JSONP, but I believe CORS is the most convenient, and the most pushed for standard.