Browser security

Same Origin Policy (SOP)

Medium
2 h 0 min

What is Same Origin Policy?

Same Origin Policy (Translated as "same origin policy") is a set of design principles that govern the implementation of browser features.

Its purpose is to isolate browser windows (and tabs) from each other, so that for example, when you go to example.com, the website cannot read your email messages from the Gmail.com site, which you may have opened in another tab.

If there were no SOP:

If the SOP (Same Origin Policy) were missing from the browser, the Internet would become a real wild west. You could have your online banking open in one tab, Facebook in another, and someone's blog in a third. The blog could act on your behalf in the online banking, steal all your money, and even post a Facebook update about the robbery on your wall.

SOP prevents websites from different origins from stealing data or performing unwanted actions on each other.

What is Origin?

The definition of origin is simple. Two websites have the same origin if their scheme (http://, https://, etc.), host (e.g. www.example.com), and port (e.g. 443) are the same. You can find the definition from RFC6454 - Document (Web Origin Concept).

Implicit ports

If ports are not specified, it is implicitly 80 for HTTP and 443 for HTTPS. You can read more about the URL structure here.

Examples

These URL addresses are of the same origin (all schema is https, host www.hakatemia.fi and port 443):

  • https://www.hakatemia.fi/
  • https://www.hakatemia.fi/courses/browser-security/same-origin-policy-sop
  • https://www.hakatemia.fi:443/courses/browser-security/same-origin-policy-sop

And these addresses are of different origin:

  • http://www.hakatemia.fi/ (wrong schema and wrong port)
  • https://hakatemia.fi/ (wrong host)
  • https://hakatemia.org/ (incorrect host)
  • https://www.hakatemia.fi:8080/ (wrong port)

What does SOP allow and what doesn't it?

Usually writing is allowed and reading is prohibited. What exactly this means depends on the browser's feature, so let's look at some examples.

Using the JavaScript window

There are many ways a website can gain access to another window's handle. However, you can limit this by using the COOP (Cross-Origin Opener Policy) and CSP (Content Security Policy) frame-ancestors directive.

These methods include, for example,

  • By using window.open.
  • Creating the frame (as we are doing).
  • Using Windows.opener if a website is framed by another one.
  • Received postMessage event.source.

This knob provides access to stripped versions of the window and location objects.

Conducting some experiments at http://a.local. We start by acquiring the window handle at address http://b.local by creating a cross-origin frame as follows:

var crossOriginFrame = document.createElement('iframe');
crossOriginFrame.src = 'http://b.local';
document.body.appendChild(crossOriginFrame);
var handle = crossOriginFrame.contentWindow

Let's see what we can do with it.

βœ‹ Not allowed: Read content from different origin

console.log(handle.document.body.innerHTML);

❌ Uncaught DOMException: Blocked a frame with origin "http://a.local" from accessing a cross-origin frame.

βœ‹ Not Allowed: Write content from different origin

handle.document.body.innerHTML = &quot;<h1> Hacked</h1> &quot;;

❌ Uncaught DOMException: Blocked a frame with origin "http://a.local" from accessing a cross-origin frame.

πŸ‘ Allowed: Read the number of frames in a cross-origin iframe

console.log(handle.frames.length);

βœ… 2

☠ Security impact: In some cases, the rendering of frames allows for simple information leakage from a foreign site.

βœ‹ Not allowed: Read URIs from multiple sources

console.log(handle.location.href);

❌ Untamed DOME exception: Blocked frame from origin "http://a.local" from accessing a cross-origin frame.

πŸ‘ Allowed: Write URIs with the same beginnings

handle.location.replace('https://www.example.com')

☠ Security Impact: Web pages that you frame on your website can access the window.opener property, meaning that if you load a malicious website into an iframe on your website, the iframe can change your website's URI to, for example, a phishing page (a clone of your page that steals your users' passwords or forces them to download something malicious). You can prevent this by using sandboxed iframe frames.

πŸ‘ Allowed: Posting messages to window via postMessage

The PostMessage method allows cross-origin windows to communicate with each other.

// at http://b.local/
window.addEventListener(
  'message',
  (event) => {
    document.write('Got message: ' + event.data)
  },
  false
)

// at http://a.local/
handle.postMessage('hello', 'http://b.local')

βœ‹ Not allowed: Read localStorage or sessionStorage

console.log(handle.localStorage);

❌ Uncaught DOMException: Blocked a frame with origin "http://a.local" from accessing a cross-origin frame.

console.log(handle.sessionStorage);

❌ Uncaught DOMException: Blocked a frame with origin "http://a.local" from accessing a cross-origin frame.

Embedding Resources and JavaScript Usage

Usually it is allowed to embed any resource (image, style, script, etc.) between different origins, but JavaScript cannot directly access a resource. However, you can restrict this with CORP (Cross-Origin Resource Policy).

In addition, when embedding resources, the browser user's cookies are sent along with the request to the embedded resource's website. This allows websites to send identifying information (via cookies) in inter-site GET and HEAD requests.

☠ Security Impact: The fact that browsers send cookies with these requests enables CSRF attacks (Cross-Site Request Forgery) if your website allows actions to be performed (e.g. transferring money, changing passwords, deleting accounts) through GET requests (which it should not allow, of course).

Let's take a look at a few examples of inter-site resources.

πŸ‘ Allowed: displaying images

<img id="cross-origin-image" src="http://b.local/apina.png" />

πŸ‘ Allowed: Create a canvas from an image

A canvas object can be created from a downloaded image of foreign origin.

var crossOriginImage = document.getElementById('cross-origin-image')
var Canvas = document.createElement('canvas')
canvas.width = crossOriginImage.width
canvas.height = crossOriginImage.height
Canvas
  .getContext('2d')
  .drawImage(crossOriginImage, 0, 0, crossOriginImage.width, crossOriginImage.height)
document.body.appendChild(canvas)

βœ‹ Not allowed: read pixels from canvas

This is not possible, the script does not have direct access to data from a foreign origin. In this case, the browser throws an error message that the canvas is "contaminated" with data loaded from a foreign source, so JavaScript does not have access to read the pixels of the canvas.

canvas.getContext('2d').getImageData(1, 1, 1, 1).data;

❌ Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted with different origin information.

πŸ‘ Allow: Loading Styles

Styles can of course be loaded from foreign sources.

<link rel="stylesheet" href="http://b.local/test.css"/>

βœ‹ Not allowed: Read style content

However, you cannot directly read CSS rules of styles downloaded from an external source using JavaScript.

console.log(document.styleSheets[0].cssRules);

❌ Uncaught DOMException: Failed to read the 'cssRules' property from 'CSSStyleSheet': Cannot access rules at <anonymous>:1:25

πŸ‘ Allowed: Script loading

<script id="cross-origin-script" src="http://b.local/test.js"></script>

Here is the content of test.js:

var x = 5

βœ‹ Not allowed: Reading the source code of the script

It is not possible to read the source code of a script loaded from an external origin using JavaScript.

πŸ‘ Allowed: Access to the information and functions provided by the script

In the script, there was the x-variable, do you remember? We can now use it on our site.

console.log(x)
5

JSONP worked basically like this (do not use it, it has never been a good idea, and nowadays we have better ways, which you will see in a moment).

☠ Security Impact: Allowing browsers to access data/functions provided by scripts between domains enables XSSI attacks (Cross-Site Script Inclusion) if your website offers dynamic JavaScript files with authenticated user data. Therefore, do not generate scripts on-the-fly with user data; scripts should be static.

HTML Forms

Earlier, we examined how embedding resources downloaded from unknown sources allowed malicious websites to send GET requests containing cookies on behalf of the browser user. Now you will see how HTML forms enable sending cookies (such as session identifiers) with POST requests.

☠ Security Impact: This activity is the primary reason for the existence of CSRF vulnerabilities. Fortunately, the situation is finally improving as SameSite cookies are starting to be enabled by default.

πŸ‘ Allowed: Send a URL encoded HTML form with cookies

Assuming that we have the following form at http://a.local and the user has an active session at http://b.local:

<form method="POST" action="http://b.local/transferFunds"><input name="amount" type="text" value="10000" /><input name="iban" type="text" value="HACKERBANK1337" /><input type="submit" value="Send" /></form>

When the user clicks on the "Send" button, this HTTP request is sent to the address http://b.local:

HTTP-PyyntΓΆ
POST /transferFunds HTTP/1.1
Host: b.local
Cookie: SESSIONID=s3cr3t
Content-Type: application/x-www-form-urlencoded
...
amount=10000&iban=HACKERBANK1337

And the application sends the money, imagining that the request came from the user themselves.

πŸ‘ Allowed: Send a multipart-formatted HTML form with cookies

Like an URL-encoded form, a multipart form can be sent from a foreign website without any issues.

<form method="POST" action="http://b.local/transferFunds" enctype="multipart/form-data"><input name="amount" type="text" value="10000" /><input name="iban" type="text" value="HACKERBANK1337" /><input type="submit" value="Send" /></form>

Not Allowed: Submit JSON HTML form with cookies

The content type "application/json" cannot be used to send POST requests with cookies from a foreign website. Cookies are not sent at all with this type of HTTP request.

☠ Security Impact: If the application does not properly check the content-type of the HTTP message, it may interpret such a POST request as a valid JSON message.

XHR and Fetch requests

πŸ‘ Allowed: Sending cookie-enabled GET, HEAD, and POST requests with XHR

The following works. You will receive an error message, but the request will be sent. You can verify with your browser's developer tools or, even better, configure a proxy server, such as Burp Suite, between the browser and the web server to see what happens.

let xhr = new XMLHttpRequest()
xhr.withCredentials = true
xhr.open('GET', 'http://b.local/')
xhr.send()

πŸ‘ Allowed: Sending GET, HEAD, and POST requests containing cookies with Fetch API

Fetch usage works the same way.

fetch('http://b.local/', { method: 'POST', credentials: 'include' })

βœ‹ Not allowed: Read XHR response

When using XHR or Fetch to send an HTTP request to a site of foreign origin, you cannot read the response of your HTTP request.

❌ Cross-Origin request blocked: The same-origin policy prevents reading the remote resource at http://b.local/. (Reason: CORS header "Access-Control-Allow-Origin" missing).

Let's see in a moment what Access-Control-Allow-Origin means.

βœ‹ Prohibited: Sending PUT, PATCH, DELETE, etc. requests

Only certain HTTP verbs are allowed by default (GET, POST, HEAD, and OPTIONS).

fetch('http://b.local/', {method: 'PUT', credentials: 'include'});

❌ The CORS policy has prevented a request from http://a.local to http://b.local: the preflight request did not pass the access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

This error has two interesting parts. It talks about a preflight request and request mode. I will soon return to the preflight matter, but let's quickly look at the request modes first.

Request Modes

Web applications can use a specific request state to prevent unnecessary data leakage in accidental requests, for example, by explicitly setting the state to the same origin (same-origin), causing the browser to throw an error if the request is attempted to be made to a foreign origin.

βœ‹ Not Allowed: Sending JSON Requests

Only content types added to the list of allowed content types are allowed. This does not work:

fetch('http://b.local/', {
    method: 'POST', credentials: 'include', headers: {
        'Content-Type': 'application/json'
    },
});

❌ The CORS policy has prevented a request from http://b.local/ from a source 'http://a.local': the preflight response does not pass the access control check: No 'Access-Control-Allow-Origin' header is requested in the resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource when CORS is not in use.

WebSockets

πŸ‘ Allowed: Opening a WebSocket connection to a foreign origin, reading from it, and writing to it

This may come as a surprise, but the same-origin policy does not restrict WebSockets.

☠ Security Impact: If an application using websockets does not verify the Origin header during websocket handshake or implement some other CSRF protection mechanism, a malicious website can open a websocket connection and use it as the browser user.

Summary

The SOP is the foundation of browser security. It is outdated and not perfect, so developers must take its shortcomings into account when designing applications.

Usually writing, like sending POST requests to a foreign website, is allowed, but reading, like for example checking the response of that POST request, is typically prohibited.

Developers can partly mitigate the same-origin policy using Cross-Origin Resource Sharing (CORS) headers, but they should do so carefully and avoid CORS altogether if possible.

The practice of same origin can also be strengthened in newer browsers with the help of CORP (Cross-Origin Resource Policy) and COOP (Cross-Origin Opener Policy).

Finally, and somewhat surprisingly, WebSockets are not protected by SOP at all. This can have unexpected and unpleasant effects if you are not careful in their implementation.

In the following module, we will familiarize ourselves with CORS.

hakatemia pro

Ready to become an ethical hacker?
Start today.

As a member of Hakatemia you get unlimited access to Hakatemia modules, exercises and tools, and you get access to the Hakatemia Discord channel where you can ask for help from both instructors and other Hakatemia members.