Skip to content

Same-origin policy and storage security

An origin is the combination of three components:

  1. Schemehttps or http
  2. Host — the domain or IP address
  3. Port — explicit (:8080) or implicit (443 for HTTPS, 80 for HTTP)

All three must match for two URLs to be considered the same origin. The table below shows examples relative to https://example.com:

URLSame origin?Reason
https://example.com/pageYesIdentical scheme, host, port
https://example.com:443/xYesPort 443 is implicit for HTTPS
http://example.comNoDifferent scheme
https://www.example.comNoDifferent host (subdomain)
https://example.com:8080NoDifferent port
https://api.example.comNoDifferent host

You can read the current page’s origin in JavaScript:

console.log(window.location.origin);
// e.g. "https://example.com"

How the same-origin policy isolates storage

Section titled “How the same-origin policy isolates storage”

The browser enforces origin isolation for every client-side store: localStorage, sessionStorage, IndexedDB, the Cache API, and OPFS are all partitioned by origin. A script running on https://example.com cannot read or write the storage of https://other.com — the browser simply will not allow it.

This is not something you configure; it is enforced at the platform level with no opt-out.

flowchart LR
  subgraph OriginA["Origin A — https://example.com"]
    direction TB
    SA1[(localStorage)]
    SA2[(IndexedDB)]
    SA3[(Cache API)]
  end
  subgraph OriginB["Origin B — https://other.com"]
    direction TB
    SB1[(localStorage)]
    SB2[(IndexedDB)]
    SB3[(Cache API)]
  end
  OriginA -.->|blocked by SOP| OriginB
Each origin has its own isolated storage partition — cross-origin reads are blocked

HTTP vs HTTPS: different origins, different stores

Section titled “HTTP vs HTTPS: different origins, different stores”

Because the scheme is part of the origin, http://example.com and https://example.com are two different origins with completely separate storage buckets. Data written to localStorage over HTTP will not be visible over HTTPS and vice versa.

This is a common source of confusion during local development: if you test over HTTP and then switch to HTTPS (or the reverse), any previously stored data will appear to vanish. It has not been deleted — it is stored under a different origin key.

A page can embed an <iframe> from a different origin. That iframe’s scripts run in the context of the iframe’s origin, so they read and write that origin’s storage — not the parent page’s storage. There is no way for the parent and the iframe to share localStorage directly across different origins.

The XSS risk: never store secrets in Web Storage

Section titled “The XSS risk: never store secrets in Web Storage”

Cross-site scripting (XSS) is an attack where malicious JavaScript is injected into and executed on your page. Any script that runs on your origin — including injected scripts — can read your entire localStorage and sessionStorage with a single call:

// An attacker's injected script can do this:
const stolen = JSON.stringify(localStorage);
// then exfiltrate `stolen` to an attacker-controlled server

This means you must never store access tokens, refresh tokens, API keys, or passwords in localStorage or sessionStorage. If an attacker finds any XSS vector (a third-party script, a misconfigured CSP, an unsanitised DOM insertion), they can silently drain every secret stored there.

Use HttpOnly cookies for session tokens instead

Section titled “Use HttpOnly cookies for session tokens instead”

Cookies with the HttpOnly flag cannot be read by JavaScript at all — not even by injected scripts. The browser attaches them to requests but hides them from document.cookie. Combined with Secure (HTTPS-only) and SameSite=Strict or SameSite=Lax, HttpOnly cookies are the correct place for session tokens.

// This is what the server sets — JS cannot read it:
// Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax
Which three components together define a browser origin?
A page stores data in localStorage over http://example.com. The site migrates to HTTPS. What happens to that data?
Why should session tokens not be stored in localStorage?