Some documents in a browser have an opaque origin rather than a normal scheme/host/port one. What is an opaque origin, which documents get one, and what stops working for script running inside such a document?
answer
- not a tuple at all
- unique per creation
- serializes to the string null
- never matches anything, even itself
- storage access throws, it does not return empty
basics
~20 sAn opaque origin is an internal unique value with no scheme, host or port. It serializes to the string "null", is never same-origin with anything — not even a second copy of itself — so origin-keyed storage such as localStorage and IndexedDB throws SecurityError there.
solid answer
~50 sAn opaque origin is an internal, unguessable value the browser mints instead of a scheme/host/port tuple. Its defining property is that it is **never same-origin with any other origin, including a second document created identically**. It serializes to the literal string `"null"`, so `location.origin` reads `"null"` and outgoing requests carry `Origin: null`. Documents get one when they are loaded into an `<iframe>` whose `sandbox` attribute omits `allow-same-origin`, when they come from a `data:` URL, and when a response carries a CSP `sandbox` directive. The practical consequence is that everything keyed by origin is gone: touching `localStorage` or `sessionStorage` throws a `SecurityError`, `indexedDB.open()` fails, cookies are unavailable, and a service worker cannot be registered. On the server side, `Origin: null` must never be treated as trusted, because any page on the internet can produce one from a sandboxed frame.
go deeper
Recall that some documents — sandboxed frames and data: URL documents — get an origin that matches nothing, shows up as the string "null", and has no storage of its own.
Explain that the origin is a unique internal value rather than a tuple, name the contexts that produce one, and know that web storage and IndexedDB access throws SecurityError there instead of returning empty.
Diagnose from the symptoms — location.origin reading "null", a SecurityError on the first storage read, Origin: null in server logs — and explain why granting a frame its real origin back is a trust decision, not a configuration tweak.
Own where untrusted content runs. Decide which embedded content must be denied a real origin at all, how state reaches it once storage is gone, and make sure no service anywhere treats a null origin as a member of the allowlist.
## What "opaque" means Every browsing context needs an origin so that security checks have something to compare. But some documents must not be given a real one — they are untrusted or their URL carries no meaningful authority. For those, the browser mints an **opaque origin**: an internal value with no scheme, host or port, unique per creation and not derivable from anything the page can see. The key property is total isolation. An opaque origin fails a same-origin check against *every* other origin, including another opaque origin — even one created a millisecond earlier from the exact same URL by the same parent. There is no way to make two of them match, and no way to widen one. When an opaque origin has to be written down — in `location.origin`, in `self.origin`, in the `Origin` request header — it serializes to the four-character string `"null"`. That serialization is lossy on purpose: it deliberately reveals nothing, which is also why it cannot be used to identify anyone. ## Where they come from - A document loaded into an `<iframe>` whose `sandbox` attribute is present without the `allow-same-origin` token. This is the common one: the frame gets an opaque origin even if it was served from your own host. - A document from a `data:` URL. There is no host to derive an origin from, so the browser assigns an opaque one. - A response delivered with a Content-Security-Policy `sandbox` directive, which applies the same treatment to a top-level document. A related case runs the opposite way and is worth knowing so you do not confuse it: `about:blank` and `javascript:` URLs *inherit* the origin of whatever created them rather than getting an opaque one. So an empty iframe you create and then write into is same-origin with you, while a `data:` URL frame is not. ## What stops working Everything the platform keys by origin, which is more than people expect: ```js // inside a document with an opaque origin location.origin; // "null" localStorage.getItem("k"); // throws DOMException: SecurityError indexedDB.open("db"); // fails - no origin to key the database to ``` Web storage (`localStorage`, `sessionStorage`) throws a `SecurityError` on access rather than returning empty — the failure is a thrown exception, not a silent miss, which is why a script that merely *reads* a cached value crashes at its first line inside such a frame. IndexedDB cannot open a database because databases are named within an origin. Cookies are unavailable. A service worker cannot be registered, since registration is scoped to an origin. And the document cannot reach a same-origin sibling, because nothing is same-origin with it. Cross-document messaging still works, but a receiving page cannot allowlist the sender by origin — the sender's origin arrives as `"null"` on the event, which is indistinguishable from every other opaque sender. The only workable pattern is to compare the source window reference against a frame you created yourself. ## On the wire Requests from an opaque origin carry the header `Origin: null`. This is where opaque origins turn into a server-side hazard: a service that keeps an allowlist of permitted origins and includes `null` in it has effectively allowed everyone, because any attacker's page can spawn a sandboxed frame and issue requests from a null origin. `null` is not "no origin" and it is not "local file"; it is "an origin deliberately stripped of identity", and it must be treated as untrusted. ## Working with them If you are the one sandboxing content, an opaque origin is exactly what you want — that is the whole reason for omitting `allow-same-origin`. Accept that the framed document has no storage and design it to receive whatever state it needs from its embedder by message. If you are debugging one, the tells are unmistakable: `location.origin` reads `"null"`, storage access throws `SecurityError` instead of returning `null`, and server logs show `Origin: null`. And if you find yourself wanting to give the frame its storage back, understand what the fix costs: granting `allow-same-origin` to content served from your own host hands that content your real origin, and with it your storage and your DOM.
- Two iframes in one page load the exact same data: URL. Can script in one reach the other?No. Each document gets its own freshly minted opaque origin, so the same-origin check between them fails despite the identical URL. Opaque origins are unique per creation, not derived from the URL, and there is no mechanism to make two of them match.
- A server allowlists the value null as a permitted origin. What is wrong with that?It allows everyone. `null` is not a specific party — any page on the internet can produce it by making a request from a sandboxed frame or a `data:` URL document. Treating it as trusted converts an allowlist into an open door, so `null` belongs on no allowlist.
- Why does reading localStorage in such a document throw rather than return null?Because there is no origin to key a storage area to, so the platform cannot give you an empty store either — it refuses the access with a `SecurityError` DOMException. Code that assumes storage is always at least readable will crash on its very first access inside a sandboxed frame.
saying these in an interview costs you the question
- Thinks Origin: null means the request had no origin
- Assumes two identical data: URL documents share an origin
- Expects storage to read as empty rather than throw
- Believes an opaque origin can be widened or matched
- Confuses about:blank inheritance with an opaque origin