Web Platform · Web Security Model
How the Same-Origin Policy Computes and Compares Origins
The origin triple of scheme, host, and port, the opaque origins produced by sandboxing and data URLs, and which browser capabilities the policy actually gates.
A browser runs code from many unrelated parties in one address space of documents. Without a rule about which documents may inspect each other, any page that a user visits could read the contents of any other page open in another tab, submit forms with the user’s cookies, and follow the resulting redirects. The same-origin policy is the rule that prevents this, and it is deliberately narrower than people assume: it is defined over a triple of scheme, host, and port, and it constrains only some of the ways one document can reach another.
The origin is a triple, not a hostname
An origin is a tuple of three components: a scheme, a host, and a port. Two URLs share an origin only when all three match. https://example.com/a and https://example.com/b are same-origin. http://example.com/ and https://example.com/ are not, because the scheme differs. https://example.com/ and https://example.com:8443/ are not, because the port differs. https://www.example.com/ and https://example.com/ are not, because the host differs. There is no notion of a parent domain, no wildcard, and no implicit trust of subdomains.
Port comparison uses the default port for the scheme when the URL omits one. https://example.com/ is https://example.com:443/ for origin purposes; http://example.com/ normalises to port 80. A page served on http://example.com:80/ and one on http://example.com/ are therefore same-origin even though the serialisations differ.
Host comparison lowercases the host and applies IDNA processing, so an internationalised name is compared in its ASCII form (the xn-- label sequence). The host is not resolved to an IP address during the comparison. This detail matters: a page loaded from https://evil.example/ that resolves to the same server as https://bank.example/ is still cross-origin to the bank, and conversely, an origin is not tied to the server it currently points at. Domain names that change their DNS answer keep their origin identity, which is the basis of DNS rebinding attacks discussed below.
The serialisation of an origin is scheme://host:port with the port omitted when it is the default. That string appears in location.origin and in Origin request headers, and code that compares strings is doing the same comparison the browser does, provided it normalises default ports and host case first.
function originTuple(u) {
const url = new URL(u); // normalises host case and default port
return `${url.protocol}//${url.host}`; // includes :port only when non-default
}
originTuple('https://Example.COM:443/a'); // "https://example.com"
originTuple('http://example.com:8080/'); // "http://example.com:8080"
Opaque origins and the documents that have none
Some documents have no tuple origin at all. They are assigned a fresh, globally unique opaque origin each time they are created, and an opaque origin is never same-origin with anything, including another opaque origin created from the same source. The important cases:
- A
data:URL navigated to directly receives an opaque origin. - A
blob:URL inherits the origin of the environment that created it, so a blob created byhttps://example.comis same-origin with that page — this is why blob URLs are a common vector for script injection. - An
<iframe sandbox="...">withoutallow-same-origingets an opaque origin even when itssrcis a normal HTTPS page. Removing that token is the strongest single thing thesandboxattribute does. about:blankandabout:srcdocinherit the origin of the parent document that created them, which is why anabout:blankframe is scriptable by its opener.
The asymmetry between blob: (inherits) and data: (opaque) has practical consequences. Trusting a data: URL to carry an identity is never sound; trusting a blob: URL is sound only as far as the creator’s origin is trusted, and the URL itself can be leaked to other origins as a capability because possession of the URL is sufficient to load it.
file: URLs are the long-standing exception. The URL Standard defers their origin to the implementation, and browsers differ: some treat every file as an opaque origin, some treat the whole file system as one origin, and some allow configuration. Code that relies on local files sharing an origin works on some browsers and not others.
Where the boundary is enforced
The policy is not one check in one place. Each capability has its own enforcement point, and they do not all use the same notion of identity.
Cross-document DOM access is the classic case. A script holding a WindowProxy for another frame may call a small allowlist of properties — postMessage, blur, focus, close, closed, frames, length, top, opener, parent, self, window, location (assignment and replace) — on a cross-origin window. Every other property read or write throws a SecurityError. The Document of another origin cannot be reached at all, which is why iframe.contentDocument is null for a cross-origin frame.
Network reads are gated separately. fetch and XMLHttpRequest may send a cross-origin request, but the response body is withheld from the script unless the server opts in with CORS headers; the mechanism is treated in CORS preflight and credentialed requests. Cookies are not governed by the origin triple at all but by a host-and-path model with its own site-level controls, covered in cookie scoping and the site boundary. Storage APIs — localStorage, IndexedDB, Cache Storage — are keyed by origin, so two origins on the same host do not share them.
Many capabilities are not restricted by origin at all, and this is the part most often misremembered:
- A cross-origin image, stylesheet, script, font, or media file can be embedded and executed (subject to CORS for some module and font cases).
- A page can navigate any frame or window it holds a reference to, including a top-level window it opened, and can submit a form to any origin.
- A cross-origin frame’s presence is observable:
onloadfires andlengthis readable. window.openreturns a handle whoseclosedproperty is readable across origins.
The security model is therefore about reading, not about reaching. An attacker who can get markup onto a page can make the browser issue requests to any origin; what they cannot do is read the response. That asymmetry is the reason CSRF exists and why it needs its own defence, as described in CSRF defences and the SameSite interaction.
document.domain and the cost of relaxing the boundary
Before postMessage was widely usable, pages relaxed the policy by setting document.domain to a common parent, which allowed a.example.com and b.example.com to script each other. The setter has two side effects that surprise people. First, the origin’s port becomes null, so after setting document.domain, the page is same-origin with any port on that domain. Second, the relaxation is one-directional in effect but sticky: a page that has lowered its domain cannot raise it again.
Both effects weaken the boundary more than the syntax suggests. Lowering to a parent domain that hosts user content — a shared hosting suffix, a blog platform, a customer-specific subdomain — hands script access to every other page under that parent. The setter is deprecated, and browsers have moved toward treating it as a no-op with a console warning; the Origin-Agent-Cluster: ?1 response header opts a page into origin-keyed agent clusters precisely so that document.domain is not available and the origin boundary is not blurred. New code should use postMessage with explicit origin checks, the subject of sandboxed iframes and the postMessage boundary.
What the policy does not protect
The origin boundary is a policy enforced by the browser’s own code, not a hardware boundary. Several classes of attack sit outside it.
DNS rebinding defeats the host component without touching the tuple: an attacker serves a page from attacker.example, then changes that name’s A record to point at a router or a development server on the victim’s local network. The page’s origin is unchanged, so its fetch calls to http://attacker.example/ now reach the internal device, and the response is same-origin readable. Defences are on the server side — validating the Host header, requiring a token the attacker cannot obtain — because the browser has no way to distinguish the two answers.
Cross-site leaks exploit the fact that presence, timing, and size are observable even when contents are not. Frame counting, window.length, the onload timing of an image, cache probing, and error events all leak a bit or more per query. These are statistical attacks that require many requests, and they are the practical reason high-resolution timers and shared memory are withheld from documents that are not cross-origin isolated. Site isolation and the limits of the origin boundary covers the process-level response.
Finally, the policy assumes the origin is a trustworthy unit. An origin that serves user-uploaded HTML, or that runs third-party scripts with full access to its DOM, is an origin whose contents are effectively controlled by others. Splitting such content onto a separate origin is the only structural fix; CSP and Trusted Types reduce the chance of injection but do not change who owns the origin once injection succeeds.
References
- HTML Standard — Origin — WHATWG
- URL Standard — Origin — WHATWG
- RFC 6454: The Web Origin Concept — IETF