The same-origin policy is often summarised as "a page cannot talk to another origin". In the browser, what does the same-origin policy actually prevent, and which cross-origin behaviours has it never prevented?
answer
- sending was never the problem
- reads are blocked, not requests
- img, script and form posts still fire
- cross-document DOM access throws SecurityError
- the server still sees the request
basics
~20 sThe same-origin policy blocks reading, not sending. It stops a page from touching another origin's DOM, response bodies and storage, but it never stopped it from loading cross-origin images and scripts, submitting forms, or navigating — those requests still go out with cookies.
solid answer
~50 sThe policy is an *isolation* rule about reading, not a firewall about sending. Three families of reads are blocked: cross-document access (touching `frame.contentWindow.document`, or a cross-origin stylesheet's `cssRules`, throws a `SecurityError`), reading the body or headers of a cross-origin `fetch`/`XMLHttpRequest` response unless the server explicitly opts in, and reading another origin's storage — `localStorage`, `sessionStorage`, IndexedDB, cookies. What it has *never* blocked is embedding and sending: `<img src>`, `<script src>`, `<link rel=stylesheet>`, `<iframe>` and `<video>` all load cross-origin freely, a form can POST to any origin, and any page can navigate you anywhere. Those requests carry the target origin's cookies subject to cookie rules. So a cross-origin request is typically *sent* and has its full effect on the server; the browser simply withholds the response from the calling script. That asymmetry is exactly why server-side state-changing endpoints cannot rely on the browser to protect them.
go deeper
Recall the one-line rule: the browser blocks reading another origin's data, not sending requests to it. Be able to name a blocked case (reading a cross-origin response body) and an allowed one (loading a cross-origin image).
Explain the three blocked families — cross-document access, response contents, storage — and show why embedding and form posts were never in scope. Know that a blocked read still means the server processed the request.
Use the asymmetry when you debug: decide from the Network panel whether the send or the read failed, and say why state-changing endpoints must verify intent themselves rather than leaning on browser isolation.
Own the consequences at system level: which third-party scripts you accept into your origin at all, how you bound the coarse side channels that free embedding leaves open, and where isolation must be enforced by architecture rather than by the browser.
## What the policy is really guarding The same-origin policy exists because a browser runs code from many mutually distrusting parties in one process tree, holding credentials for all of them. Its job is to make sure that documents from origin A cannot *observe* anything belonging to origin B. Read that word carefully: the guarantee is about observation. It is narrower and stranger than "a page cannot talk to another origin", and most confusion about browser security comes from assuming the broader rule. ## The three families it blocks **Cross-document access.** If a page embeds an iframe from another origin, the parent may hold a reference to `iframe.contentWindow`, but reaching through it to `.document` throws a `SecurityError` DOMException. The same applies in reverse via `window.parent`, and to sibling frames. A small set of `Window` members stays reachable across origins by design — `postMessage`, `closed`, `length`, `frames`, `top`, `parent`, `opener`, `close`, `focus`, `blur`, and `location` as a *write* target — and that list is deliberately tiny and non-revealing. The rule shows up in less obvious places too: a stylesheet loaded from another origin appears in `document.styleSheets`, but reading its `cssRules` throws, because the rules could encode information the embedding page should not have. **Response contents.** A `fetch()` or `XMLHttpRequest` to another origin will not hand the response body, or most of its headers, to your script unless the responding server opts in with the appropriate response headers. In `no-cors` mode the request still goes out and you get an *opaque* response object — status 0, no readable body — which is useful only for handing to a cache or an `<img>`. **Storage and credentials.** `localStorage`, `sessionStorage` and IndexedDB are partitioned by origin; you can only ever see your own. Cookies are partitioned by host and path. There is no API to enumerate another origin's storage. ## What it never blocked The web was already full of cross-origin *composition* when the policy was written, and breaking that was never an option. So all of the following still work with no permission from anybody: - Embedding subresources: `<img src>`, `<script src>`, `<link rel=stylesheet>`, `<iframe>`, `<video>`, `<audio>`. (Web fonts are the odd one out — cross-origin `@font-face` files do require a CORS opt-in.) - Submitting a `<form>` to any origin, with any method. - Navigating the top-level window, or a frame you own, to any URL. - Sending a plain cross-origin `fetch` in `no-cors` mode. In every one of these cases the request leaves the browser, carries whatever cookies the cookie rules allow, and the destination server processes it normally. What the page does *not* get is the answer. A cross-origin `<script src>` is the extreme case: the response is not readable as data, yet it is executed with your origin's privileges — which is why you only include scripts from parties you trust as much as yourself, and why runtime errors from such a script are sanitized to the bare string `"Script error."` unless the tag carries `crossorigin` and the server sends matching headers. ## Why the asymmetry Once you see that requests go out but answers do not come back, two large facts about the web fall out. First, *the browser is not protecting your server*. Any page anywhere can cause a logged-in user's browser to issue a cookie-bearing POST to your endpoint. The attacker cannot read the response, but if the request alone changes state, the damage is done. State-changing endpoints therefore have to verify intent themselves; the origin policy will not do it for them. Second, side channels are the interesting attack surface. Because embedding is free, a page can often measure *whether* a cross-origin load succeeded, how long it took, or what dimensions an image had — small leaks that follow directly from "you may send and you may observe coarse outcomes, but you may not read". Browser hardening work over the last decade has largely been about narrowing those coarse observations. ## What this means in practice When a request fails in DevTools with a cross-origin message, check *which* half failed. If the network panel shows a real status code and the failure is in your JavaScript, the request succeeded and only the read was denied — the fix belongs on the server's response headers. If nothing hit the server at all, something stopped it before it left. And when you need two of your own origins to cooperate, the platform's supported channel is explicit messaging between the documents, not reaching through a frame reference.
- If a cross-origin fetch was blocked, why does the request still appear in the Network panel with a real status code?Because the policy is enforced on the *response handoff*, not on the send. For a simple cross-origin request the browser dispatches it, the server processes it and answers, and only then does the browser decide your script may not see the body. The network row is the truth about what the server did; the JavaScript error is about what you were allowed to read.
- A page includes a third-party script with <script src>. What does that script get access to?Everything your origin has. A script executes in the including document's origin regardless of where its bytes came from — same DOM, same cookies, same storage, same fetch privileges. The response body is not readable *as data*, but it is fully privileged *as code*, which is why third-party script inclusion is a trust decision, not a networking one.
- Which Window properties remain readable across origins, and why is that list so short?Roughly `postMessage`, `closed`, `length`, `frames`, `top`, `parent`, `opener`, `close`, `focus`, `blur`, and `location` as a write-only target. Each either carries no information about the other document's contents or is a deliberate cooperation channel. Anything that would reveal what the other origin loaded — its DOM, its URL, its stylesheet rules — is excluded.
saying these in an interview costs you the question
- Claims the browser prevents cross-origin requests from being sent
- Thinks a blocked read means the server never saw the request
- Says the policy protects the server from unwanted writes
- Believes a third-party script runs in the third party's origin
- Assumes cross-origin images and forms are blocked too