David Dunn

Senior Software Developer


Back to Blog

Browser Security Fundamentals: SOP, CORS & Secure Contexts

11 min read
security javascript

As frontend developers we need to understand security policies the browser imposes, why it imposes them, what happens if they're violated and how to debug issues around them. In this post we will go over these topics and understand why it is important we're aware of them and also tackle the dreaded CORS error.

As frontend engineers we often can forget where our code will be run, the browser.

While we don’t need to build our own browser we do need to understand, at some level, how it works.

Some common issues many frontend developers encounter:

We’re not going to memorise headers here, but we do want to build an understanding of:

We will start with origins.

Origins 101: What is the Same Origin?

What is an origin within the context of a browser? Well it is part of the URL:

Origin = scheme + host + port

https://example.com:1234
URLSame origin as https://app.example.com?Why?
https://app.example.comtrueidentical
http://app.example.comfalsedifferent scheme (http vs https)
https://api.example.comfalsedifferent host (api vs app)
https://app.example.com:1234falsedifferent port
https://example.comfalsedifferent host

We should also confirm domain vs subdomain vs origin:

So when we’re thinking about browser security here we only care about the origin, not the domain.

But why? Why does the browser care about origins? Well, in situations where a user has multiple sites open at once, those sites must not be able to steal each others secrets.

Secrets such as:

The Same-Origin Policy is what protects our app from such threats.

Same-Origin Policy

The Same-Origin Policy, or SOP for short, states that scripts can only fully interact with resources from the same origin.

So what does this mean?

Lets look at some examples based on some actions a we may perfom in code:

ActionCross-origin allowed?Notes
Click <a href="https://other.com">trueNavigation is always allowed
<img src="https://other.com/pixel">trueImage loads; JS can’t read pixel data without extra APIs
<script src="https://cdn.other.com/app.js">trueScript can run, but now runs with your origin’s power
fetch("https://api.other.com")false (read blocked)Request is sent; JS can’t read the response (without CORS)
iframe.contentWindow.documentfalseDOM access blocked unless same origin
document.cookie for other originfalseEach origin has its own cookie jar

It is important to note here that the network request is not blocked. The browser makes the request but SOP prevents our JavaScript code from reading it. This is why we often see the response body in the network tab of DevTools but within our code we get a CORS error. The browser has the data but it refuses to hand it over to JS due to SOP.

SOP underpins a few things we may encounter as developers:

CORS is also a very common interview question, so we will dig more into that now.

Cross Origin Resource Sharing - (CORS)

In simple terms, CORS is about ‘who’ can read the response. It does not block requests to the server, this is why we can use tools like Postman to test our server API but we can still get CORS errors in the browser. The browser decides whether JS can see the response based on CORS.

Browsers catergorises cross-origin requests into Simple or Preflighted requests.

Preflighted Requests

A preflighted request is one where the browser first sends an OPTIONS request to the server with headers such as:

The server will then respond with the response containing Access-Control-Allow-* headers, which the browser parses. If the browser is satisfied that the two origins can communicate then the actual request is sent.

The following tends to trigger a preflight request:

If the preflight fails then the actual request is never sent.

Simple Requests

Simple requests do not send the OPTIONS request, the browser will just send the actual request.

Assuming we follow some strict rules the following will not trigger a preflight and stay simple:

If the request remains simple and the server responds with the Access-Control-Allow-Origin header containing our origin then the browser will allow JS to read the response.

CORS Headers

Now we’ve mentioned a few headers related to CORS but what do they actually mean? First, we as Frontend developers do not usually set these but we must be able to read and reason about them when interacting with APIs.

So, in plain English this is how the browser will interpret particular headers:

So why do we frontend developers need to worry about these if we never set them? And who actually sets them?

Typically backend and/or devops engineers will set these headers on our app servers, proxy layers, API gateways etc etc. We, as frontend developers, will then interact with these services from our code, and if the headers are missing or have been set incorrectly then we will see the CORS error messages.

So we need to be able to debug these error messages, understand what is going wrong and then inform the backend/infra teams of the issue. We’re often in the best position to debug these issues, and it is always better to provide a detailed explanation of what is wrong rather than just saying: ‘there is a CORS issue here, can you please fix’.

The Access-Control-Allow-Origin: * + credentials trap

Previously we discussed that Access-Control-Allow-Origin can be set to a specific origin or * for everyone. Then we also discussed Access-Control-Allow-Credentials which will allow handling of authorization.

So what happens if we set both of these? For example:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Well technically what we’re saying here is that we allow any website to read sensitive, authenticated, data from our API if the user happens to be logged in. Yes this is as scary as it sounds. It is so bad that Browsers enforce a rule:

If Access-Control-Allow-Credentials: true, then Access-Control-Allow-Origin must be a specific origin, not *.

CORS debugging as Frontend Developers

So how do we debug these CORS errors and provide useful information to our fellow developers?

Let’s say we’re developing a React application running at http://localhost:3000 which calls APIs located at https://api.example.com and we get a CORS error.

  1. Check the request’s Origin header (DevTools → Network). Is it what we expect? (http://localhost:3000/https://app.example.com)
  2. Check the response headers from the API:
    • Is Access-Control-Allow-Origin present?
    • Does it match the Origin exactly?
    • Or is it * when we’re using credentials?
  3. If we’re sending cookies or Authorization:
    • In JS: credentials: "include" (fetch) or withCredentials: true (XHR/Axios).
    • On server:
      • Access-Control-Allow-Credentials: true
      • Access-Control-Allow-Origin: https://app.example.com (not *)
  4. If we’re using non-simple methods/headers:
    • Check the preflight (OPTIONS) response:
      • Access-Control-Allow-Methods includes the method you want.
      • Access-Control-Allow-Headers includes your custom headers.

Secure Context and HTTPS

Secure Context is required by many APIs such as:

But what is a secure context?

A secure context is the browser saying:

“I trust this page enough to give it powerful capabilities.”

In technical terms this means:

  1. The page is loaded from a trusted origin:
    • Usually over HTTPS
    • http:localhost when in dev
  2. The page is not undermined by insecure content it pulls in:
    • No blocked active mixed content such as scripts via plain HTTP

When a page is not considered a secure context the browser will block or disable certian APIs.

Now we have a basic understanding of what secure contexts are we can dig into why we need them.

Consider the following APIs:

Now imagine if any random page a user navigated to could access those APIs… Just randomly turn on a users microphone or camera. Its a huge security violation and this is why we need secure contexts, so browsers know it is safe to expose those APIs.

A simple rule browsers follow is: “No HTTPS, no powerful APIs”

If we try to call the getUserMedia() API outside of a secure context we’d get an error like this:

getUserMedia() is only available in secure contexts

Now lets look at a more complicated example. We have a page being served over HTTPS but on this page there is an iframe that loads resources from an http page. What happens? Are we in a secure context or not?

The answer is, in most cases, we’re not in a secure context. We touched on this breifly above when we stated ‘The page is not undermined by insecure content it pulls in’.

Mixed content here refers to an HTTPS page that is pulling resources from an HTTP page. In this there are two types of mixed content:

  1. Passive mixed content Resources such as:

    • Audio
    • Video
    • Images These aren’t as serious as active, and historically browsers have allowed these with warnings. Why? Well because they only affect what the user can see, not what the underlying JS code is doing. Its still generally considered a bad idea and policies around this are getting stricter so we should always try to avoid loading our resources from HTTP where possible.
  2. Active mixed content Common examples:

    • <script src="http://…">
    • <link rel="stylesheet" href="http://…">
    • <iframe src="http://…">
    • XHR/fetch/WebSocket to http://… from an HTTPS page

    These are generally blocked by the browser outright as they’re very dangerous as an attacker could tamper with the unsecure HTTP traffic.

Conclusion

There we have it, an introduction into browser security fundamentals. Security, as one might expect, is a huge topic and has many applications in the lifecycle of an application. In this post we only skimmed the surface, even in the context of web development.

In later posts we will dive into more security topics, and potentially dive deeper into the ones we discussed here!

For now we just need to remember that as frontend developers we must have some understanding of security concerns that are within our domain so that we can:

  1. Write better, more secure code.
  2. Debug CORS errors with confidence, and answer CORS related interview questions (they will come up).
  3. Provide better explanations to our fellow developers about the nature of CORS, and other security concerns.