Same-origin policy and storage security
What is an origin?
Section titled “What is an origin?”An origin is the combination of three components:
- Scheme —
httpsorhttp - Host — the domain or IP address
- 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:
| URL | Same origin? | Reason |
|---|---|---|
https://example.com/page | Yes | Identical scheme, host, port |
https://example.com:443/x | Yes | Port 443 is implicit for HTTPS |
http://example.com | No | Different scheme |
https://www.example.com | No | Different host (subdomain) |
https://example.com:8080 | No | Different port |
https://api.example.com | No | Different 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 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.
Cross-origin iframes
Section titled “Cross-origin iframes”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 serverThis 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