A picture of a ever repeating pattern

Easing the Cross-Origin pain on the Web

I’ve been developing front-end applications all my life. Nevertheless, every time I experience a CORS issue, I feel deep pain. Today I’ll try to ease that pain for all of us!

TL;DR

  • It is a front-end dev’s problem that can only be fixed by a backend dev
  • The Same Origin Policy is only enforced by the browsers (front-end)
  • CORS has to be enabled by on the server (back-end)
  • It is important that in the server response, the `Access-Control-Allow-Origin` header is set.
  • It is very important that in the browsers request headers the `Origin` header is set.
  • If you experience any issues with so called tainted content, check whether one of those headers is missing.
  • https://enable-cors.org/index.html is a website that explains how to enable CORS on most backend technologies.

TOC

Understanding

1 What the f* is CORS anyways?

Usage

2 Cross-Origin Resource Sharing (CORS) to the rescue!

Avoiding

3 How to bypass the Same Origin Policy (SOP) and avoid CORS?

End

4 Further Reading
5 Final Words

What the f* is CORS anyways?

To understand Cross-Origin Resource Sharing (CORS) we have to understand the Same-Origin Policy:

Same Origin Policy (SOP)

The Same-Origin Policy is a web-browsers’ standard that prevents malicious external scripts to access sensitive data from one web-page. Generally it only permits a web-page to load data that comes from the same origin. For example if a web-page on the origin www.foo.com/bar is requesting an image from www.foo.com/image.png it will work by default since the resource is on the same origin as the request comes from. But, when www.foo.com loads resources from www.baz.com they will either be tainted (meaning they can be displayed but not read from in JavaScript) or forbidden completely by the same origin policy since baz.com is not the same origin as foo.com. An origin is defined as a combination of URI scheme, domain, and port number.

So, before CORS came into play, it was almost impossible to load resources from other web-pages. Now CORS is a mechanism that make this sharing possible. We’ll see how in a minute.

How is the Same Origin Policy useful?

It’s a concept to isolate one website from another. It is what stops for example Facebook accessing the data from your bank account. This rule is enforced by the browser. If it’s not a browser, then the Same Origin Policy does not apply.

Examples of the Same Origin Policy

You could for example from website A open another website B and read/manipulate its content like so:

// on www.hacker.com
const bank = window.open('https://www.yourbank.com/transactions', 'right');
console.log(bank.document.body); // same origin “error”

Here we are on the website hacker.com, this website opens a new browser tab for us which loads yourbank.com (suppose that’s the name of our bank). Now suppose I’m logged in already, hacker.com could read all information of that page. Or worse, could send me to the banks official login page but capture my credentials on the way!

Luckily the Same Origin Policy prevents that. So the actual output will be an error similar to: Error: Blocked a frame with origin "https://www.hacker.com/" from accessing a cross-origin frame. Which is a good example on how one website/window/origin is isolated from another by the Same Origin Policy.

The same could be done using frames:

<!-- on www.hacker.com -->
<frameset cols="50%,50%">
  <frame name="framea" src="https://www.hacker.com/hack" />
  <frame name="frameb" src="https://www.yourbank.com/transactions" />
</frameset>
<script>
  console.log(window.framea.document.body); // works
  console.log(window.frameb.document.body); // same origin “error”
</script>

Here is some ancient example of frames. This is how websites used to be ~15 years ago. Although the same principles apply on iframes, which you are probably more familiar with. Same as with example one, because of the Same Origin Policy the website hacker.com can only access the contents of framea (since it is on the same origin) but not frameb!

Images, Video, CSS and JS:

Any image, video, CSS and JS code can be loaded and executed from any other site but not read. The image will be shown, the video will be shown and the CSS will be applied. However, you won’t be able to read the contents with JavaScript:

<!-- on www.hacker.com -->
<link rel="stylesheet" type="text/css" href="hacker.css" />
<link
  rel="stylesheet"
  type="text/css"
  href="https://www.yourbank.com/transactions.css"
/>
<script>
  console.log(document.styleSheets[0].rules); // works
  console.log(document.styleSheets[1].rules); // null (empty)
</script>

The same example applies to images, videos and JavaScript code. This is done to protect you in case there was any sensitive information in an image/video/css

HTTP Requests:

// on www.hacker.com
fetch('https://hacker.com/info', { method: 'POST', body: 'hi' }); // works
fetch('https://yourbank.com/transactions', { method: 'POST', body: 'hi' }); // does not

Note that the request is actually happening: the browser sends the request to your bank, the browser will receive the response from your bank, but the browser will not let JavaScript access that response. You will get some kind of No 'Access-Control-Allow-Origin' Headers present error message instead.

As you can see, the Same Origin Policy is a set of rules implemented by the browsers that protect one web-page from others in the same browser. It isolated pages from one another.

So should we keep enforcing the Same Origin Policy?

It depends. While it might be another layer of security, I can totally aggree with ttwinlakkes: most of the times I have had problems with it is with protected APIs that already have protections in place. It might be a good idea in general, but it is a nuisance if your only cross-origin resources are protected public endpoints.

As you can see it really depends on your use case: Enabling Cross Origin Request for all domains is not a security risc per se and can make total sense. Generally, for 99% of public APIs the Same Origin Policy makes absolutely no sense.

Cross-Origin Resource Sharing (CORS) to the rescue!

With time came the conclusion that it is very often required to load resources from other origins. For example, you might want to host your API endpoint on another origin than your web-app, i.e. api.foo.com vs app.foo.com.
CORS is basically how you say “Do not apply the Same Origin Policy here”. I’ll explain exactly how in a bit.

When is CORS triggered?

Whenever you make a HTTP Request to:

  • a different domain (i.e. foo.com calls baz.com)
  • a different subdomain (i.e. bar.foo.com calls bat.baz.com)
  • a different port (i.e. foo.com calls foo.com:9000)
  • a different protocol (i.e. https://foo.com calls http://foo.com).

What requests can use CORS?

  • Any XMLHttpRequest or Fetch requests.
  • Web Fonts (as @font-face within CSS).
  • WebGL textures.
  • Images, Videos, Scripts and Links when the crossorigin attribute is set.

How to use CORS correctly?

CORS allows a site to “opt-in” to weaken SOP and allow external pages to access and read data.

What Headers do we need?

Access-Control-Allow-Origin

Restricts which origins are allowed to make a request. Use * to allow access from all origins or use a specific origin:

Access-Control-Allow-Origin: https://developer.mozilla.org

If you require the client to pass authentication headers (e.g. cookies) the value can not be * — it must be a fully qualified domain!

Access-Control-Allow-Methods

Which HTTP Methods are allowed to be made:

Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, PATCH, DELETE

Access-Control-Allow-Credentials

Only required if your server supports cookie auth.

Access-Control-Allow-Credentials: true

More access control headers:

These are less common, still, here is the list:

For Requests:

  • Access-Control-Request-Headers Request header is used by browsers when issuing a preflight request, to let the server know which Headers will be used when the actual request is made.
  • Access-Control-Request-Method Request header is used by browsers when issuing a preflight request, to let the server know which HTTP method will be used when the actual request is made.

How to enable CORS on my Server?

The fabulous website enable-cors.org has a list on how to enable CORS for Apache, nginx, PHP, Tomcat, Caddy, Meteor and many more. For NodeJS you can either write them yourself or use this nice express middleware.

const express = require('express');
const app = express();

app.use((req, res, next) => {
  res.append('Access-Control-Allow-Origin', ['*']);
  res.append(
    'Access-Control-Allow-Methods',
    'GET,PUT,POST,DELETE,OPTIONS,PATCH'
  );
  next();
});

app.post('/', async (req, res) => {
  res.send('ok');
});

app.listen(3000, function() {
  console.log('Example app listening on port 3000!');
});

In HTML, there is an attribute for that! It’s called crossorigin and here is how you use it:

<img crossorigin="anonymous" src="https://foo.com/img" />
<!-- OR -->
<img crossorigin="use-credentials" src="https://foo.com/img" />
<!-- Also works with other resources: -->
<script
  crossorigin="anonymous"
  src="https://example.com/example-framework.js"
></script>
<link crossorigin="use-credentials" rel="manifest" href="/app.webmanifest" />
<video
  crossorigin="anonymous"
  src="https://example.com/videofile.ogg"
  autoplay
  poster="posterimage.jpg"
></video>

Note:

  • If you load those resources without the crossorigin working, they are referred as tainted. I.e. a Cross Origin Image that was included without respecting CORS is referred as Tainted Image and its manipulation is restricted.
  • The Server still has to have CORS enabled and send the image with an Access-Control-Allow-Credentials Header
  • In some older browsers and in React it is very important that the crossorigin attribute is set before the src attribute otherwise it might be ignored!

How to bypass the Same Origin Policy (SOP) and avoid CORS?

PostMessage Communication

PostMessage allow sites to “opt-in” to remove SOP.

  • Sender specifies where it sends to.
  • Receiver checks which origin it received the message from.

Example

// Sender (parentsite)
window.frameA.postMessage(message, 'http://siteA.com');
// Receiver (siteA)
window.addEventListener('message', event => {
  if (event.origin !== 'http://parentsite.com') {
    console.log('wrong origin');
  } else {
    document.body.innerText = event.data;
  }
});

However, the relationship has to match the origin of the receiver. Read more: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

Use a proxy

Since it is only the browser that enforces the SOP you can place a Server in the middle of your request:

  • Sender sends the request a Server on the same origin or with CORS enabled
  • The Server redirects the request to the external recipient
  • The Server gets the response and passes it back to the Sender

Why does it work? Because as described above, SOP doesn’t apply server-to-server communication, only to browser-to-server.

Example

var proxy = require('express-http-proxy');
var app = require('express')();

app.use('/proxy', proxy('www.google.com'));

This is a super basic example of a basic Proxy server in NodeJS with express using express-http-proxy. It takes any request to the /proxy endpoint and passes it through to www.google.com, returning the response to the caller.

Other Proxies

Further Reading

If you would like tho know more about CORS and SOP, I can recommend following resources for a deep dive:

Final words

Congratulations! You just learned all you have to know about CORS and SOP.

PS: don’t forget to share the knowledge and to follow me on twitter or via email if you liked this post :)

4 Great Posts

Comments