Web Security

How Browser Security Works: Same-Origin Policy, CORS, and the Sandbox

A technical deep-dive into browser security: how the Same-Origin Policy is defined (scheme+host+port), what it blocks and allows, how CORS extends it, the browser sandbox model, and Site Isolation.

November 15, 20258 min readShipSafer Team

The Same-Origin Policy is one of the most important security mechanisms on the web, yet many developers who work with it daily can't fully explain when it fires, what it blocks, or why CORS works the way it does. This post builds a complete mental model: SOP definition, the exact rules for what SOP allows and blocks, how CORS extends SOP for cross-origin resource sharing, the browser sandbox, and Chrome's Site Isolation architecture.

What Is an "Origin"?

An origin is the combination of three things: scheme (protocol), host (domain), and port. All three must match for two URLs to be considered same-origin.

URL AURL BSame Origin?Reason
https://example.com/page1https://example.com/page2YesPath doesn't matter
https://example.comhttp://example.comNoDifferent scheme
https://example.comhttps://sub.example.comNoDifferent host
https://example.com:443https://example.comYesPort 443 is implicit for HTTPS
https://example.com:8080https://example.comNoDifferent port
https://example.comhttps://example.com:443YesExplicit and implicit match

The path, query string, and fragment are never part of the origin. https://example.com/app and https://example.com/admin are the same origin.

What SOP Allows and Blocks

The Same-Origin Policy is not a single rule — it applies differently to different types of browser operations.

What SOP Allows (No Restrictions)

Embedding cross-origin resources: SOP does not block embedding. These always work:

<!-- Image from a different origin -->
<img src="https://cdn.other.com/image.png" />

<!-- Script from a CDN -->
<script src="https://cdn.jsdelivr.net/npm/react@18/index.js"></script>

<!-- Stylesheet from Google Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter" />

<!-- iFrame embedding another site -->
<iframe src="https://example.com/widget"></iframe>

<!-- Form submission to a different origin (no SOP restriction) -->
<form action="https://other.com/submit" method="POST">

Navigation: window.location = 'https://other.com' works from any origin.

Form submissions: POSTing a form to a different origin works (this is what CSRF exploits).

What SOP Blocks

Reading responses from cross-origin fetch/XHR: This is the core of SOP.

// From https://myapp.com, this fetch WILL be sent
fetch('https://api.other.com/data')
  .then(res => res.json())  // BLOCKED — cannot read the response
  .catch(err => console.log('CORS error:', err));

The request is sent — the server receives it and responds. SOP blocks the JavaScript from reading the response. This is why SOP alone doesn't protect against CSRF (the form submission goes through), only against unauthorized data reads.

Cross-origin DOM access: JavaScript in https://myapp.com cannot read the DOM of a cross-origin iframe, and vice versa.

const iframe = document.getElementById('crossOriginIframe');
// SOP blocks this:
iframe.contentDocument.querySelector('input[name="password"]');
// Throws: Blocked a frame from accessing a cross-origin frame

Cross-origin cookie reading: document.cookie only returns cookies for the current origin. JavaScript at evil.com cannot read cookies set by bank.com. (However, note that cookies have their own SameSite / Secure / HttpOnly attributes that interact with but are separate from SOP.)

Cross-origin localStorage/sessionStorage: Each origin has its own isolated storage. localStorage data at bank.com is invisible to any other origin.

The null Origin Edge Case

Some contexts have a null origin: file:// URLs, locally sandboxed iframes (sandbox attribute without allow-same-origin), and data: URLs. Be careful with CORS policies that allow Origin: null — this can be abused.

CORS: Extending SOP for Cross-Origin Reads

CORS (Cross-Origin Resource Sharing) is a mechanism that lets servers declare which origins are allowed to read their responses. It is purely a browser enforcement mechanism — the server sends headers and the browser decides whether to give the response to JavaScript.

Simple Requests vs Preflight Requests

Simple requests (GET/POST/HEAD with simple headers) go directly to the server:

GET /api/data HTTP/1.1
Origin: https://myapp.com
Host: api.other.com

The server responds:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Content-Type: application/json

{"data": "..."}

If Access-Control-Allow-Origin does not include https://myapp.com, the browser discards the response.

Preflight requests are sent automatically by the browser before requests that could have side effects — any method other than GET/POST/HEAD, or requests with custom headers like Authorization or Content-Type: application/json.

OPTIONS /api/data HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
Host: api.other.com

Server preflight response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

If the preflight is rejected, the actual DELETE request is never sent.

CORS Headers Reference

HeaderDirectionPurpose
Access-Control-Allow-OriginResponseWhich origins can read the response
Access-Control-Allow-MethodsPreflight responseAllowed HTTP methods
Access-Control-Allow-HeadersPreflight responseAllowed request headers
Access-Control-Allow-CredentialsResponseWhether cookies/auth can be sent
Access-Control-Max-AgePreflight responseHow long to cache preflight
Access-Control-Expose-HeadersResponseWhich response headers JS can read
OriginRequestThe origin making the request

The Wildcard + Credentials Problem

You cannot use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. The browser will reject the combination because it would allow any site to make authenticated cross-origin requests with the user's cookies.

// Browser rejects this combination:
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Credentials: true

// Must use a specific origin when credentials are involved:
// Access-Control-Allow-Origin: https://myapp.com
// Access-Control-Allow-Credentials: true

Implementing CORS correctly in Express:

import cors from 'cors';

const ALLOWED_ORIGINS = [
  'https://myapp.com',
  'https://www.myapp.com',
  process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null,
].filter(Boolean);

app.use(cors({
  origin: (origin, callback) => {
    // Allow server-to-server requests (no origin header)
    if (!origin) return callback(null, true);

    if (ALLOWED_ORIGINS.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`CORS: origin ${origin} not allowed`));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400,
}));

CORS Misconfiguration: Reflecting the Origin

A common misconfiguration: reflecting the Origin header directly into Access-Control-Allow-Origin:

// WRONG: allows any origin to make credentialed requests
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
  res.header('Access-Control-Allow-Credentials', 'true');
  next();
});

This is functionally identical to * with credentials. An attacker at evil.com can make requests to your API using the victim's cookies.

The Browser Sandbox

Modern browsers run web content in a sandbox that restricts access to the operating system even if malicious code executes in the page context. The sandbox model is defense-in-depth: assume an XSS vulnerability exists, but limit what attackers can do with it.

Process Isolation

Chrome and Edge (Chromium-based) use a multi-process architecture:

  • Browser process: Controls UI, manages tabs, orchestrates other processes
  • Renderer process: Runs web content (HTML, CSS, JavaScript) for one or more tabs
  • GPU process: Handles GPU-accelerated rendering
  • Network service: Handles network requests
  • Plugin processes: Isolated processes for browser extensions

Renderer processes run in a restricted sandbox. They cannot access the filesystem, open network sockets directly, or call most OS APIs. If an attacker exploits a JavaScript engine vulnerability and gains code execution in the renderer, they still need a second "sandbox escape" vulnerability to affect the OS.

Site Isolation

Chrome's Site Isolation (enabled by default since Chrome 67) allocates one renderer process per site, not per tab. Before Site Isolation, multiple same-site tabs shared a renderer process — a Spectre/Meltdown-style attack could read memory across tabs.

With Site Isolation:

  • bank.com and evil.com are in different renderer processes
  • A compromised renderer for evil.com cannot read the memory of bank.com's renderer
  • Cross-origin iframes get their own renderer process, even if embedded in a same-origin page

Content Security Policy and the Sandbox

CSP's sandbox directive applies the browser sandbox to a page regardless of origin:

Content-Security-Policy: sandbox allow-scripts allow-forms

For iframes, the HTML sandbox attribute:

<!-- Maximum restriction: no scripts, no forms, no same-origin, no navigation -->
<iframe src="https://untrusted.com/widget" sandbox></iframe>

<!-- Allow scripts but keep cross-origin isolation -->
<iframe
  src="https://untrusted.com/widget"
  sandbox="allow-scripts"
></iframe>

<!-- Note: 'allow-same-origin' + 'allow-scripts' defeats the sandbox —
     the iframe can reach its own origin's storage and cookies -->

Cross-Origin Isolation (COOP/COEP)

For access to powerful APIs like SharedArrayBuffer (required for WebAssembly threads), pages must opt into cross-origin isolation:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

COOP: same-origin puts the page in its own browsing context group, isolating it from popups opened by cross-origin sites. COEP: require-corp blocks loading of cross-origin resources that don't explicitly opt in via Cross-Origin-Resource-Policy.

Testing cross-origin isolation:

// Check if cross-origin isolation is active
console.log(crossOriginIsolated);  // true if both COOP and COEP are set

// SharedArrayBuffer is only available when crossOriginIsolated is true
const sab = new SharedArrayBuffer(1024);

Understanding the browser security model at this level helps you make informed decisions about CORS policies, CSP configurations, and iframe isolation — all of which have security implications that are easy to get wrong without knowing why the browser behaves the way it does.

browser security
same-origin policy
cors
sandbox
site isolation
web security

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.