Has been blocked by cors policy [Explain like I am 5]

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 resource and who blocks access

Imagine a browser requests a resource which is located on a domain b.com from page served on a.com . For example image from HTML <img src="http://b.com/image.jpg" , font loaded from CSS src: url("b.com/font.ttf"), or even REST API call made from JavaScript executing on a.com/xx page.

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

In the example, 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 has been blocked by cors policy🛑.

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

CORS should be implemented on side of b.com web-server, and only there.

Two kinds of requests

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

Simple requests are:

And only that of these which have one of the next values in Content-Type header (you could check it in Chrome DevTools -> Network tab):

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 browser are OK, we are talking only about headers set by you from your request maker like XHR/fetch/axios/superagent/jQuery Ajax. BTW request maker can set it without your agreement, so better start with pure browser-native XHR of fetch API, don't use superagent/axios unless you know why you need it. If you need to set a header by yourself still and still want keep 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 (e.g. has some extra non-standard headers in request). If you worry about extra cases refer to browser documentation, e.g. https://developer.mozilla.org/en-US/docs/Web/HTTP/AccesscontrolCORS#Preflighted_requests

CORS on simple requests

To allow CORS, web-server, in responses to simple requests should add HTTP header that describes what set of origins 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. Browser asks web-server for resources regardless of the same or different domains are used.
  2. Web-server always answers with content but can add some extra headers, or may not. The base header is Access-Control-Allow-Origin (ACAO) that should contain allowed origin, e.g. a.com, or wildcard * that will allow all origins.
  3. Only then browser searches for ACAO 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 developers console, e.g. if there is no headers at all: XMLHttpRequest cannot load ... No 'Access-Control-Allow-Origin' header is present...:

Has been blocked by cors policy error example

So blocking performed by browser by reading response headers. Most of browser even have some flag like chrome.exe --disable-web-security and you will not see an errors like this.

🤔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.

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

And you as 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

Main point here, assumed, that non-simple method can change data on a server. So performing things in the way above is unacceptable - first, we will change data on the server (e.g. make a bank transaction) and only then verify access? Of course no. So before making a non-get request 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.

Let's consider preflight in details:

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 then browser makes 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 simplicity of becoming a developer without understanding how it works.

Now think 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 in GET method handlers. They will be treated like simple! No preflight at at all. Developer start earning good money on development start working in big companies or at freelance finds a good client. (Client does not understand what is security, team leads are also can't always think about it). Developed product is more popular and popular, and more it popular more hackers attention will be there. Hacker finds URL makes more research, finds some clients of a product, creates a.com with same look and typo in domain and BOOM, he has an access.

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

How to attach cookies on request

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 version can be something like this:

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

In this case, 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

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

This is well-known rule known as content-type enforcement or application/json enforcement. Just raise an exception immediately if 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 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" 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 has 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 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 a binary files because it has 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 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 same problem - hacker can place a form with hidden inputs on own site and when user will click on some button, if he authorized on yours 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!

Has been blocked by cors policy explanation

#security #cors
24
Ivan Borshchov profile picture
Jan 28, 2020
by Ivan Borshchov
Did it help you?
Yes !
No

Best related

Other by Ivan Borshchov