skip to content

Sandboxing, COOP/COEP, and Site Isolation

You will learn how the browser separates untrusted content at the process and document level — site isolation and the header pair that earns you cross-origin isolation. Interviewers ask this for embed-heavy products and anywhere SharedArrayBuffer appears.

on this pageshow

questions

5

A partner says your page refuses to render inside their <iframe>. Which two HTTP response headers control whether a document may be framed, and how does X-Frame-Options differ from the Content-Security-Policy frame-ancestors directive?

level: juniorimportance: should knowfreq 46%

answer

  1. framing is opt-out, not blocked
  2. legacy header is all-or-nothing
  3. source list beats DENY or SAMEORIGIN
  4. every ancestor is checked
  5. meta tag delivery is ignored

basics

~10 s

Two response headers decide it: the legacy X-Frame-Options, which only offers DENY or SAMEORIGIN, and the Content-Security-Policy directive frame-ancestors, which takes a source list. Where both appear, browsers honour frame-ancestors and ignore X-Frame-Options.

solid answer

~40 s

Framing is opt-out: nothing stops a page from putting your URL in an `<iframe>`, so the framed document has to refuse in its own response headers. The old mechanism is `X-Frame-Options`, which is all-or-nothing — `DENY` or `SAMEORIGIN`. Its `ALLOW-FROM` value never shipped in Chromium or WebKit, so it cannot express "only this partner". The current mechanism is CSP: `Content-Security-Policy: frame-ancestors 'none' | 'self' | https://partner.example`, which takes a real source list and is checked against every ancestor document in the chain, not just the immediate parent. Two gotchas: `frame-ancestors` is ignored when delivered in a `<meta http-equiv>` tag, so it must be a genuine header; and when both headers are present, browsers that support `frame-ancestors` ignore `X-Frame-Options` entirely — so a permissive CSP silently overrides a strict `DENY`.

code

bash · 1 line
bash
curl -sI https://app.example/report | grep -iE 'x-frame-options|content-security-policy'

go deeper

for a junior

Know both header names and be able to say that the framed page, not the embedder, decides. Recall that X-Frame-Options offers only DENY and SAMEORIGIN, while frame-ancestors takes a list of allowed origins.

for a middle

Explain the mechanics: the whole ancestor chain is checked, meta-delivered policies do not enforce this directive, and frame-ancestors takes precedence when both headers are present rather than the two being combined.

for a senior

Show you can debug it from a live response — read the console refusal, inspect headers through the CDN, and separate a genuine framing refusal from a blank frame caused by third-party cookie behaviour. Know that a permissive CSP silently overrides a legacy DENY.

for a principal

Own the policy: a deny-by-default posture with narrow, route-scoped carve-outs for named partners, plus a rule that allowlisting an origin is a trust decision reviewed like any other integration. Decide whether legacy header support is still worth the drift risk.

## What the browser is actually deciding Before a browser renders a document inside a frame, it asks whether that document consents to being embedded by the documents above it. Nothing else stops the embed: the same-origin policy governs *access* between documents, not *inclusion*, so any page on the web may put your URL in an `<iframe>` and render it. Framing is therefore opt-out, and the opt-out must come from the framed response itself — the embedder is the untrusted party here and cannot be asked to behave. The practical reason to care is that a framed page still runs with its own cookies and session. An embedder that cannot read the frame's DOM can still position it, make it transparent, and overlay bait so that a click lands on a real control inside your page. ## X-Frame-Options: the legacy switch `X-Frame-Options` predates any specification effort and offers exactly two usable values: ``` X-Frame-Options: DENY X-Frame-Options: SAMEORIGIN ``` `DENY` refuses all framing. `SAMEORIGIN` allows it only from your own origin. A third value, `ALLOW-FROM <origin>`, appeared in old Firefox and Internet Explorer but was never implemented in Chromium or WebKit, and is now obsolete — treat it as nonexistent. Consequently `X-Frame-Options` cannot express "my origin plus one named partner". It must be sent as a response header. A `<meta http-equiv="X-Frame-Options">` element does nothing. ## CSP frame-ancestors: the current mechanism ``` Content-Security-Policy: frame-ancestors 'none' Content-Security-Policy: frame-ancestors 'self' https://partner.example https://*.partner.example ``` The directive takes a source list, so scheme, host, port and host wildcards are all expressible. `'none'` corresponds to `DENY` and `'self'` to `SAMEORIGIN`, with everything in between now available. Two behavioural details matter more than the syntax: **It checks the whole ancestor chain.** If your page sits inside a frame on `b.example`, which is itself framed by `a.example`, then both `b.example` and `a.example` must match your source list. That closes the wrapping trick where an allowed partner is itself embedded by an attacker. **It is ignored in a meta tag.** `frame-ancestors`, along with `report-uri` and `sandbox`, has no effect when the policy is delivered via `<meta http-equiv="Content-Security-Policy">`. The browser has already committed to loading the document by the time it parses markup, so the decision has to live in the response headers. A policy that looks correct in the HTML source and does nothing in production is almost always this mistake. ## Precedence when both are sent Browsers that support `frame-ancestors` ignore `X-Frame-Options` when both are present. This is not a "most restrictive wins" merge. If a page sends `X-Frame-Options: DENY` alongside `Content-Security-Policy: frame-ancestors https://partner.example`, the partner embed succeeds. Teams that add a permissive CSP for one integration and leave a strict legacy header in place as a supposed backstop have, in fact, loosened the policy everywhere. Keeping `X-Frame-Options` around is still reasonable as a floor for very old user agents, but it must be kept *consistent* with the CSP, not treated as an independent guard. ## Diagnosing the partner's report Open the partner page, look at the console — browsers report a refusal to display in a frame and name the offending directive or header — and then inspect the framed URL's response headers directly: ``` curl -sI https://app.example/report | grep -iE 'x-frame-options|content-security-policy' ``` If the header is present, the fix is to add the partner origin to `frame-ancestors` (and to remove or align any `X-Frame-Options`). If it is absent, the block is coming from somewhere else — a CDN or reverse proxy injecting a default, or a different mechanism such as a cookie that is not sent in a third-party context, which produces a blank frame rather than a refusal. ## What these headers do not do They control only whether your document may be *embedded*. They say nothing about what the framed page may do once loaded, and nothing about non-document resources — an image or script of yours can still be included by any page unless you send `Cross-Origin-Resource-Policy`. They also do not apply to top-level navigation: a link to your page always works. Finally, they are enforced by the browser, so a server-side fetch of the same URL is unaffected; if the content itself must not leave your origin, authorisation, not a framing header, is the control.

  • Why does frame-ancestors do nothing when the policy is delivered in a meta tag?
    The framing decision is made before the document is parsed — the browser has to accept or refuse the embed as the response arrives. By the time a `<meta http-equiv>` element is seen, the document is already being rendered inside the frame. For the same reason `report-uri` and `sandbox` are also ignored in meta-delivered policies, so any framing control has to be a real response header.
  • If a partner is allowlisted in frame-ancestors, what else should you check before calling the integration safe?
    That the allowlisted origin is one you actually trust, since frame-ancestors grants it real UI over a session-authenticated page. Also confirm the page has no clickable state-changing actions worth stealing, that it does not accept `postMessage` from arbitrary origins, and that the allowlist is scoped to the specific routes the partner needs rather than applied site-wide.
  • Both headers are absent from a response, yet the frame renders blank in a partner's page. What else could explain it?
    Most often cookies: in a third-party context a session cookie without `SameSite=None; Secure` is not sent, so the framed page renders a logged-out or error state rather than being refused. Other candidates are a CSP on the embedder restricting `frame-src`, a redirect chain that loses the path, or the page's own script detecting `window.top !== window.self` and bailing out.

saying these in an interview costs you the question

  • Claiming X-Frame-Options ALLOW-FROM works in current browsers
  • Putting frame-ancestors in a meta tag and expecting enforcement
  • Assuming the stricter of the two headers wins
  • Thinking the same-origin policy already prevents framing
  • Believing SAMEORIGIN only checks the immediate parent today

context

open as a page

What do the response headers Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp each do to a document, and why must a page send both before window.crossOriginIsolated is true?

level: middleimportance: should knowfreq 38%

basics

~20 s

COOP: same-origin cuts the document off from cross-origin windows that could hold a reference to it; COEP: require-corp blocks cross-origin subresources that have not opted in. Both are required because crossOriginIsolated means nothing untrusted shares the process — in either direction.

open as a page

After you add Cross-Origin-Embedder-Policy: require-corp to a page, several third-party images, a font from a CDN and an analytics iframe stop loading. Explain why, and walk through the options for restoring each one.

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

require-corp blocks every cross-origin subresource that has not opted in. Restore each by getting Cross-Origin-Resource-Policy: cross-origin from the provider, refetching in CORS mode, switching to COEP: credentialless, or self-hosting. Framed documents must send their own COEP header.

open as a page

Chrome's Site Isolation and Firefox's Fission place documents from different sites in different operating-system processes. What attack made a process boundary necessary when the same-origin policy was already enforced in browser code, and what does that boundary protect that in-process checks cannot?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Spectre-class speculative-execution side channels let script read any byte in its own process, regardless of language or same-origin checks. A process boundary is the answer because it removes the foreign data from the address space entirely, rather than relying on code that speculation can bypass.

open as a page

A team wants to enable cross-origin isolation (COOP and COEP) across your product so a WebAssembly feature can use SharedArrayBuffer, but the product embeds several third-party widgets and uses a popup-based OAuth flow. How would you decide whether to do it, and how would you roll it out?

level: principalimportance: nice to knowfreq 17%

basics

~20 s

Scope it before costing it: cross-origin isolation is per-document, so isolate the route or origin that needs the capability rather than the whole product. Then inventory third-party embeds with report-only headers, and check whether the popup auth flow survives COOP.

open as a page