Browser Security Fundamentals: SOP, CORS & Secure Contexts
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:
- The famous CORS error.
- Seeing a response in DevTools but our application code can’t read it.
- When using Service Workers or geolocation and seeing something about
secure contexts
We’re not going to memorise headers here, but we do want to build an understanding of:
- What an origin is.
- What the Same-Origin Policy (SOP) actually does.
- How CORS selectively relaxes SOP.
- Why HTTPS & secure contexts now underpin modern frontend work.
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
- Scheme typically is http or https
- Host is
example.com - Port is
1234
| URL | Same origin as https://app.example.com? | Why? |
|---|---|---|
https://app.example.com | true | identical |
http://app.example.com | false | different scheme (http vs https) |
https://api.example.com | false | different host (api vs app) |
https://app.example.com:1234 | false | different port |
https://example.com | false | different host |
We should also confirm domain vs subdomain vs origin:
- Domain:
example.com - Subdomain:
app.example.com - Origin:
https://app.example.com:1234
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:
- Cookies
localStorageorsessionStorage- Response bodies from XHR or fetch
- Even the DOM of other pages
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:
| Action | Cross-origin allowed? | Notes |
|---|---|---|
Click <a href="https://other.com"> | true | Navigation is always allowed |
<img src="https://other.com/pixel"> | true | Image loads; JS can’t read pixel data without extra APIs |
<script src="https://cdn.other.com/app.js"> | true | Script 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.document | false | DOM access blocked unless same origin |
document.cookie for other origin | false | Each 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:
- iframes: Here we can embed another origin but SOP would still prevent us from accessing its DOM.
postMessage: An escape hatch to allow us to send structured messages across origins without violating SOP.- CORS: A server-side mechanism to provide exceptions to which origins SOP will allow/block.
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:
OriginAccess-Control-Request-MethodAccess-Control-Request-Headers
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:
- Methods such as
PUT,PATCH&DELETE. - Custom headers such as
X-Auth-TokenorX-Requested-With. - Non-simple
Content-Typesuch asapplication/json.
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:
- Methods such as
GET,HEADorPOST. - Headers
Accept,Accept-Language,Content-Language,Content-Typewith very specific values (application/x-www-form-urlencoded,multipart/form-data,text/plain)
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:
Access-Control-Allow-Origin: “Which Origin may read this response?” Can be a specific origin (e.g.https://app.example.com) or*(everyone).Access-Control-Allow-Credentials: “I’m willing to handle cookies/Authorization headers for this origin.” Must betrueor omitted.Access-Control-Allow-Methods: “These methods are allowed for the actual request” Methods such asGET,POST,PUTetc etc.Access-Control-Allow-Headers: “These custom headers are allowed.” Examples areContent-Type,X-Auth-TokenAccess-Control-Expose-Headers: “These response headers are visible to JS” Without this JS only sees a safe subset of 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.
- Check the request’s Origin header (DevTools → Network).
Is it what we expect? (
http://localhost:3000/https://app.example.com) - Check the response headers from the API:
- Is
Access-Control-Allow-Originpresent? - Does it match the Origin exactly?
- Or is it
*when we’re using credentials?
- Is
- If we’re sending cookies or Authorization:
- In JS:
credentials: "include"(fetch) orwithCredentials: true(XHR/Axios). - On server:
Access-Control-Allow-Credentials: trueAccess-Control-Allow-Origin:https://app.example.com(not*)
- In JS:
- If we’re using non-simple methods/headers:
- Check the preflight (OPTIONS) response:
Access-Control-Allow-Methodsincludes the method you want.Access-Control-Allow-Headersincludes your custom headers.
- Check the preflight (OPTIONS) response:
Secure Context and HTTPS
Secure Context is required by many APIs such as:
- Service Workers.
- Push Notifications.
- Some
navigator-*APIs, such as geolocation. - WebAuthn and some credential APIs
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:
- The page is loaded from a trusted origin:
- Usually over HTTPS
http:localhostwhen in dev
- 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:
- Reading from or writing to the clipboard.
- Accessing a users camera or microphone.
- Getting a users geolocation.
- Managing credentials, such as WebAuth and passwordless login.
- Registering a service worker.
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:
-
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.
-
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:
- Write better, more secure code.
- Debug CORS errors with confidence, and answer CORS related interview questions (they will come up).
- Provide better explanations to our fellow developers about the nature of CORS, and other security concerns.