Have you ever seen an error in a browser console:

Access to fetch at 'http://b.com/' from origin 'http://a.com' has been blocked by CORS

Here I will explain why it happens and how it protects a user.

Who is a resource and who blocks access

Imagine a browser requests a font or calls some REST API by using JavaScript from a page served on a.com. Both font and REST calls are resources. Imagine font or REST API is located on a domain b.com .

For example:

  • The page https://a.com/about has a font loaded from CSS src: url("b.com/font.ttf").
  • The page https://a.com/media has some JavaScript file https://a.com/script.js that inside of it's code does a REST API call using fetch/XHR/jQuery ajax. E.g. fetch("https://b.com/api/v1/stats")

Normally the browser will block the request according to the same-origin policy (SOP).

In the examples, a.com is an origin of the page which does request and b.com is an origin of the requested resource. Origins are different so the browser would normally drop an exception in console (F12 in Chrome): has been blocked by cors policy.

To remove the SOP restriction developers use a special header-based mechanism called Cross-Origin Resource Sharing (CORS).

CORS should be implemented on the side of the webserver that serves resources and only there! In our case it is b.com's webserver. Leter I will show how to implement it, but first, we need to consider more important things.

Two kinds of requests

It is very important to know that CORS works differently on two kinds of requests: simple, and non-simple.

Simple requests are:

  • GET
  • HEAD
  • POST

And only that of these which have one of the next values in Content-Type request header:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain
you could check content type in Chrome DevTools -> Network tab, click on request (Make sure no filters are Applied and All badge selected), look at Request headers

So multipart/form-data POST is simple, but application/json POST is not simple! Also application/xml POST is not simple!

☝Another tricky important condition - to be simple requests must have no manually set headers. Default headers sent by the browser are OK, we are talking only about headers set by you from your request maker (for example one of XHR/fetch/axios/superagent/jQuery Ajax etc). By the way, the request maker can set it without your agreement, so better start with pure browser-native XHR of fetch API, unless you know why you need more complex requesters.

Never use superagent/axios (or even worsen, ancient and heavy ajax) unless you know why you need it. It will save your life from a lot of issues. Don't blindly follow the guides. All request makes do the same and have very same and simple interfaces.

If you need to set a header by yourself still, and still wish to keep the request simple you are allowed to white-listed request headers and their values, they called CORS-safelisted. You can find their list and allowed values on fetch spec: https://fetch.spec.whatwg.org/#cors-safelisted-request-header

NOTE: This is a base rule, but also there might be some rare extra situations when requests are non-simple. I would say it should never happen to you. 99% of cases are covered with the rules above. So if you write a simple blog and don't see an explanation, just carefully check the rules above. However, If you are paranoid, and worry about extra cases refer to browser documentation, e.g. https://developer.mozilla.org/en-US/docs/Web/HTTP/AccesscontrolCORS#Preflighted_requests

All requests that are not simple are non-simple🙂

CORS on simple requests

To allow CORS, web-server, in responses to simple requests should add special HTTP response header that describes what set of origins which are permitted to get this resource. In the example, the origin is a.com. It all works in a CONFUSING way: when HTML or JavaScript asks for resource:

  1. The browser asks the web server for resources regardless of the same or different origins are used.
  2. Web-server should always answer with content but can add some extra headers, or may not. The base header is Access-Control-Allow-Origin (ACAO). It should contain allowed origin, e.g. a.com, or wildcard * that will allow all origins.
  3. Only then browser searches for ACAO a header in the response and if there is no header with current origin it blocks content usage in HTML or JS code and throws an error in the developer's console, e.g. if there are no headers at all: XMLHttpRequest cannot load ... No 'Access-Control-Allow-Origin' header is present...:
Image

So blocking performed by the browser after reading response headers. Most browsers even have some flag like chrome.exe --disable-web-security which disables SOP. But most times it is easier to add headers on the backend. For a good maintainable backend, it is 1 minute.

🤔You might want to ask, so if a hacker can run their browser with --disable-web-security, how then it helps at all? The thing is the hacker can't receive a benefit from attacking himself. And normal users will not do it. And even if they will, the browser will say, "Hey man, I hope you know what you are doing, it might hurt you".

SOP aim is to protect users which use official browsers with a SOP protection enabled.

And you, as a user, should always do the same, otherwise, hackers will be able to work with your web-banking via non-simple CORS requests when you are browsing sites owned by hackers (see below)!

CORS on Non-simple requests – mr. Preflight

The main point here, assumed, that a non-simple method can change data on a server. But performing things in the way above for requests which can change the data is unacceptable: first, we will change data on the server (e.g. make a credit card transaction) and only then verify access. Yes, a user on hacker's site would receive an error in the console, but who cares? Data on your server were changed, or money were sent.

So before making a non-simple request, the browser will try to make some preflight OPTIONS request which should get a response with allowed origins and only then if the origin is allowed browser will actually do a request that will change the data. So preflight itself will not change any data on the server, just will give a green or red light to browser to execute dangerous non-simple request which could change the data on server.

Let's consider preflight in details:

The client wants to do application/json POST to http://b.com/post_url and browser makes preflight:

OPTIONS http://b.com/post_url HTTP/1.1
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type 
Origin: http://a.com

ACRM and ACRH notify the server about what method will be used after preflight and what headers will be present (browser adds here Content-Type and custom headers that will be attached to XHR call).

Server answers:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://a.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400

ACAM and ACAH headers in response will say browser can it do actual method or not.

ACMA say browser that it can remember preflight for some seconds value, e.g. 86400 s = 24 h. So this means that the browser instance will not make preflights to http://b.com/post_url during the next 24 hours. BTW sometimes it is hard to reset this cache, so be careful with this header during development, better turn it to 1 second.

Only after this the browser makes actual POST:

POST http://b.com/post_url HTTP/1.1
Content-Type: application/json;
Origin: http://a.com

And in response browser also should set ACAO:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: a.com

Never change data in GET methods (and any requests that assumed to be simple)

Security is a most challenging point of development, and SOP-related attacks are super common still, because of the simplicity of becoming a developer without understanding how it works 😟.

Now think about what happens when newbie developers decide that they can always use GET because it is working anyway, start passing data via query params and change data on the server in GET method handlers. They will be treated as simple! No preflight at all. Developers start earning good money on development start working in big companies or at freelance find a a client with growing buisness. (Client does not understand what is security, team leads are also can't always think about it, such developer is the hidden bomb). The developed product is more popular and popular, and more it popular more hacker's attention will be there. Hacker finds URL and makes more research, finds some users of a product, creates a.com with the same look and typo in domain and BOOM, he has can run queries.

When you ask a new developers when to use POST and when to use GET, and they answer that POST is needed when you need to send data to the server. This is not fully true. Better to say: non-simple requests should be used when you need to change data on the server (by change I mean add, update and delete of course).

How to attach cookies on request

By default browser does not send cookies installed to the original domain (a.com). This is a great hole-fixer. For most sites, you need to attach cookies to run APIs like change passwords or withdraw money (any requests for which it is important to identify and authorize users).

It is possible to say browser that he should apply cookies saved for http://b.com .

To do this you should use withCredentials field of XMLHttpRequest request object:

var xhr= new XMLHttpRequest();
xhr.open('GET', 'http://b.com/some-url', true);
xhr.withCredentials = true;
xhr.onreadystatechange = function(resp) { conosle.log('done') };
xhr.send(); 

jQuery ajax version can be something like this:

$.ajax({
    url: 'http://b.com/some-url',
    type: 'GET',
    beforeSend: function(xhr){
       xhr.withCredentials = true;
    }
});

Or fetch version of same request:

const response = await fetch('http://b.com/some-url', {
  method: 'GET',
  credentials: 'include', // include, *same-origin, omit
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(data) // body data type must match "Content-Type" header
});

In this case, the browser will attach cookies to request, but to complete such request after response, the web-server should include in response ACAC:

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://a.com

For all requests, limit backend to accept only Content-Type: JSON

This is a well-known rule known as content-type enforcement or application/json enforcement. Just raise an exception immediately if the content-type request header is not JSON. A lot of frameworks do it for you. To understand the reason, you should know two important facts:

1. Browsers (at least most of them) does not apply SOP (and don't do Preflights as a consequence) when request made from old-school <form action='url'. Obviously due to compatibility reasons.
2. At the same time browsers can't send Content-Type application/json from form, but they still can send application/x-www-form-urlencoded or multipart/form-data or text/plain, source is in spec from whatwg.org

So if you allow application/x-www-form-urlencoded then hacker might place a <form action='yourdomain/yourapi_url' on his site and place, a submit button in the form.

Here you might think that if you are doing JSON deserialization at the beginning of your backend code, it would crash API endpoint anyway and save you, but no, there is a ENCTYPE="text/plain" the hack which will look like:

<FORM NAME="reset" ENCTYPE="text/plain" action="http://example.com/resetPassword" METHOD="POST">
    <input type="hidden" name='{"newPassword": "123456", "ignoredKey": "a' value='bc"}'>
</FORM>
<script>document.reset.submit();</script>

This snippet on hackers site would send {"newPassword": "123456", "ignoredKey": "a=bc"} to http://example.com/resetPassword so if you have an unexpired cookie stored on example.com (If you are authorized) then visiting hackers site will drop your password to 123456.

TIP: if you are trying to reproduce it to detect issue for your website, always try it in several browsers: Google Chrome, FIrefox, Opera. When some Chrome setting might prevent cookie sending due to cross-site tracking restrictions other browsers with default settings and latest versions will send it without any issues.

Pay attention that if backend inside of request handler will read the value of Content-Type header there will be text/plain not an application/json, but deserialization (e.g. JSON.parse in node or json.loads in python) would work anyway.

So, limiting Content-Type to JSON will force everyone to send only non-simple requests.

But how to protect simple requests if I want multipart/form-data?

Application-JSON content type is not efficient if you want to upload binary files because it has a limited character set and you will have to use base64 encoding which will increase traffic and upload time by ~25%, which is ok for most of the startups and you can make all endpoints better protected. But if you want to upload through optimized multipart/form-data then your requests might be simple again, and you will have to allow this content type on backed (do it for only certain APIs, not all!)

So now we have again the same problem - a hacker can place a form with hidden inputs on own site and when the user will click on some button, if he authorized on your website he will send a file. To protect from it use CSRF!

If it helped please press like or share so I will know that I need to create more hints like this!

More posts from us?

If you liked the post and wish to get a simple and clear knowledge about programming, operations, and other aspects of software development, you can read our Devforth blog, Devforth is a software development team we manage. We spent years in active commercial development (check it by our Upwork), we share our knowledge approaches and tools in "Like I am five" mode to attract more audience like you and improve awareness about us. We created posts about best practices like Critical server alerts setup, Simple and efficient Docker builds with self-hosted Docker registry, and many, many more.

Image