skip to content

Two pages, one on https://app.example.com and one on https://example.com, historically relaxed the same-origin check between them by assigning to document.domain. What did that assignment actually change, and why are browsers removing it?

level: seniorimportance: nice to knowfreq 28%

answer

  1. a legacy escape hatch
  2. widened the host, only for DOM checks
  3. both sides had to opt in
  4. the port gets blanked
  5. it blocks per-origin process isolation

basics

~20 s

Assigning document.domain widened a document's host for DOM-access checks only, and both pages had to assign it because doing so also blanks the origin's port. It never affected the Origin header, cookies or storage, and Chrome 115 disabled it by default because it breaks origin isolation.

solid answer

~50 s

Setting `document.domain = "example.com"` shortened the host used in the *same-origin-domain* check, letting two pages under one registrable domain read each other's DOM. Two details define it. First, **both** documents had to assign it — even the page already served from `example.com` — because the assignment also sets the document's origin's **port to null**, and only a matching pair of doctored origins compares equal. Second, it changed *nothing else*: the `Origin` header, cookies, storage keys and network-level checks all continued to use the real origin. Browsers are removing it because any page on any subdomain — a stale staging host, a customer-controlled vanity name — could opt itself into another subdomain's DOM, and because a mutable origin prevents the browser from committing an origin to its own isolated process. Chrome 115 (2023) made the setter a no-op by default, with the `Origin-Agent-Cluster: ?0` response header as a temporary opt-out. The supported replacement is explicit cross-document messaging, or serving both surfaces from one origin.

go deeper

for a junior

Know that this is a legacy mechanism for letting two pages under one domain script each other, that modern browsers have turned it off, and that messaging between the documents is what replaced it.

for a middle

Explain that it widened only the host used for DOM-access checks, that both documents had to assign the same value, and that the Origin header, cookies and storage keys were all left untouched.

for a senior

Articulate both removal arguments — one weak subdomain becoming a foothold, and a mutable identity preventing per-origin process isolation — and know the opt-out header exists but is temporary.

for a principal

Own the migration. Decide which surfaces genuinely need to cooperate, which move onto one origin versus a reviewed message interface, and how long you are willing to run on an opt-out header the browser intends to withdraw.

## What the assignment did The same-origin policy compares scheme, host and port. `document.domain` was a legacy escape hatch that let a document shorten the *host* half of that comparison to a suffix of its own host, as long as the suffix was a registrable domain or lower — never a public suffix such as `com` or `co.uk`. Assigning `"example.com"` from a page on `app.example.com` was permitted; assigning `"other.com"`, or `"com"`, was not. Crucially it did not change the document's origin outright. It fed a *separate* check, historically called **same-origin-domain**, which the platform consults for DOM access between documents. Two documents passed that check when their effective domains matched, regardless of the subdomains they were actually served from. ```js // on https://app.example.com document.domain = "example.com"; // on https://example.com - required even though it already matches document.domain = "example.com"; // only now can either reach the other's document ``` ## The both-sides rule, and why it exists The most-asked detail is why the page *already* on `example.com` also had to assign the value. The answer is that the assignment has a side effect: it sets the document's origin's **port component to null**. A doctored origin with a null port never matches an untouched origin with port 443, so a page that skipped the assignment stayed cross-origin with its partner no matter how similar the hosts looked. Opting in had to be mutual and explicit — which was, at least, an honest design: nobody was dragged across a boundary without saying so. ## What it never touched The mechanism was far narrower than its reputation: - The `Origin` request header kept carrying the real origin. - Cookies were unaffected; they are scoped by host and path and follow their own rules. - `localStorage`, `sessionStorage` and IndexedDB stayed keyed to the real origin, so the two pages still had separate storage. - Nothing about cross-origin network permissions changed. So it bought exactly one thing: synchronous DOM access and scripting between two documents in one browsing-context group. Teams that set it expecting shared storage or shared sessions were misreading it. ## Why it is going away Two independent reasons. **Security.** A site's registrable domain usually spans many hosts, and not all of them are equally trustworthy: an abandoned staging box, a marketing microsite on a third-party CMS, a customer-controlled vanity subdomain. Under `document.domain`, any of those could unilaterally opt into the shared domain and then script a main-application page that had also opted in. One weak subdomain became a foothold in the app. **Isolation architecture.** Browsers want to place an origin in its own operating-system process, which is a strong defence against attacks that read memory across boundaries. That commitment has to be made when a document is created — but `document.domain` meant a document's effective identity could change *later*, at any moment, which is incompatible with deciding the process up front. Chrome 115, in 2023, made the setter a no-op by default: assigning to it no longer changes anything, and the two documents stay cross-origin. A site that genuinely still depends on it can opt back in by sending the response header `Origin-Agent-Cluster: ?0`, which declares that its documents are grouped by site rather than by origin — an explicitly temporary escape hatch, not a supported long-term configuration. ## What to do instead If two of your own surfaces must cooperate, the supported route is explicit cross-document messaging between them, which keeps both origins intact and makes the data flow a reviewed interface rather than open scripting access. If they genuinely need to be one trust domain, put them on one origin and split by path instead of by subdomain. And if you are auditing an old codebase, grep for `document.domain` — the failure mode after the default flipped is silent, since the assignment succeeds and only the subsequent DOM access throws.

  • Why did the page already served from example.com still have to assign document.domain?
    Because the assignment also sets the document's origin's port component to null. A doctored origin with a null port never compares equal to an untouched one carrying port 443, so the opt-in had to be mutual — a page that skipped it stayed cross-origin with its partner regardless of the matching host.
  • Did setting document.domain give the two pages shared cookies or shared storage?
    No. It fed only the same-origin-domain check used for DOM access. The `Origin` header, cookie scoping and the storage keys for localStorage, sessionStorage and IndexedDB all continued to use the real origin, so the two documents kept entirely separate stores.
  • Could a page set document.domain to any value it wanted?
    No — only to a suffix of its own host that is a registrable domain or longer. A page on app.example.com could set `example.com`, but not `other.com` and not a public suffix like `com` or `co.uk`. Otherwise a single page could have claimed an entire top-level domain.

saying these in an interview costs you the question

  • Thinks document.domain changed the Origin header too
  • Says only one of the two pages needed to set it
  • Claims it gave the pages shared cookies or storage
  • Believes any value could be assigned to it
  • Recommends it as a current solution for subdomain cooperation

context