skip to content

Under a Content-Security-Policy, a results page renders each constituency's statement in an `<iframe srcdoc>` — which policy applies inside that frame, and why?

level: middleimportance: should knowfreq 52%

answer

  1. Local schemes have no response of their own
  2. The creator's policy travels with the document
  3. A copy, taken at creation time
  4. srcdoc, blob:, data:, scripted about:blank
  5. Opaque origin, but 'self' still resolves

basics

~10 s

A document created from a local scheme — srcdoc, blob:, data:, a scripted about:blank — and a worker created from such a URL inherit a snapshot copy of the creating document's Content-Security-Policy list.

solid answer

~40 s

The frame is under a **copy of the creating document's policy list**, taken at the moment the frame's document is created. Documents from local schemes — `srcdoc`, `blob:`, `data:`, an `about:blank` document scripted into existence — and workers created from such URLs have no response of their own to carry a `Content-Security-Policy` header field, so without inheritance a page could escape its own policy simply by framing markup it fully controls. Because it is a copy and not a live link, later changes on either side do not propagate in either direction. The copied list also carries a **self-origin**, so `'self'` still resolves even though such a document can run in an opaque origin.

code

html · 6 lines
html
<!-- This document was served with:
     Content-Security-Policy: script-src 'nonce-r4nd0mPerResponse' -->
<iframe
  srcdoc="&lt;script&gt;init()&lt;/script&gt;
          &lt;p&gt;Turnout 61.4%&lt;/p&gt;"&gt;
</iframe>

go deeper

for a junior

Recall the headline: markup you put in a srcdoc frame is still under your page's policy, so inline script there is blocked just as it is on the page. It is not a way around the policy.

for a middle

Explain the rule by creation method: documents from srcdoc, blob:, data: and a scripted about:blank, plus workers from such URLs, take a copy of the creator's policy list, while anything fetched over the network brings its own.

for a senior

Show that you know it is a snapshot. Given a worker created from a blob: URL and a policy tightened afterwards, say that the worker keeps the copy it was created with, and name what that means for rolling a policy change out.

for a principal

The judgment here is about composed content. Decide whether untrusted third-party markup belongs in a context that inherits your policy or in one fetched from an origin with a policy of its own, because the two put the enforcement boundary in different places.

## The hole inheritance closes A policy is normally attached to a document by the response that delivered it: the browser reads the `Content-Security-Policy` field lines off the response and builds the document's policy list from them. That works for anything fetched over the network — and leaves a hole for documents that were never fetched. Consider the results page. It is served with a strict policy that forbids inline script. It then writes markup into an `<iframe srcdoc>`. That frame's document has no response, no header fields and therefore, on the naive model, no policy at all. If that were true, the page could place any inline script it liked inside a `srcdoc` frame and the script would run — and so would any inline script an attacker got into the markup that the page composes. The framed-content escape is the specification's own motivating example, and inheritance is the rule that closes it. ## Which contexts inherit, and which do not The rule turns on **how the document was created**, not on whether it is same-origin with anything: | Context | Where its policy list comes from | |---|---| | A frame loaded from `https://…` | its own response's `Content-Security-Policy` field lines — empty if it has none | | `<iframe srcdoc="…">` | a copy of the creating document's list | | A document from a `blob:` URL | a copy of the creating document's list | | A document from a `data:` URL | a copy of the creating document's list | | An `about:blank` document scripted into existence | a copy of the creating document's list | | A worker created from a `blob:` URL | a copy of the creating document's list | | A worker created from `https://…` | its own response's `Content-Security-Policy` field lines | The first row is the one candidates get backwards. A frame served from a real URL does **not** inherit anything from the page that framed it. It brings whatever its own response brought, which may be nothing at all. Inheritance exists precisely for the contexts that have no response of their own. ## It is a copy, not a link The inherited list is a **snapshot taken at creation time**. Three consequences follow, and all three are asked about: 1. Changing the creating document's policy after the fact — by adding a policy in a `<meta http-equiv="Content-Security-Policy">` element, for example — does not reach a frame or worker that already exists. 2. A policy added inside the created document does not travel back to its creator. 3. Nothing about the relationship is live: there is no ongoing propagation in either direction once the copy is made. So a worker created from a `blob:` URL enforces the policies that were on its creator when it was created, for the whole of its life, regardless of what the creating document does afterwards. ## Why the list carries a self-origin A document created from a local scheme can end up in an **opaque origin** — an origin that is not equal to any other, including its creator's. That is a problem for one specific source expression: `'self'`. A keyword-source that means "the document's own origin" has nothing useful to resolve to in an opaque origin, and a policy of `script-src 'self'` inherited into such a document would match nothing at all. The CSP list therefore carries a **self-origin** alongside its policies, so that `'self'` in an inherited policy still resolves to the origin the policy came from. Note what this does not do: storing that origin does not make the created document's origin any less opaque. It exists so the inherited policy remains meaningful, not to change the document's origin. ## A policy written inside the created document A `<meta http-equiv="Content-Security-Policy">` element inside the `srcdoc` markup binds **that frame's document**. It is appended to that document's own policy list, which already holds the inherited copy, so the frame then has to satisfy both. It does not reach the parent, and it cannot loosen the inherited copy — the two are separate members of one list and each is enforced on its own terms. ## Getting it wrong - Assuming every frame inherits from its parent, and being surprised when a framed third-party document loads scripts your policy forbids. - Assuming a `srcdoc` frame is unconstrained, and shipping inline script into it. - Assuming the relationship is live, and expecting a policy tightened on the page to reach a worker already running. - Assuming an opaque-origin document silently loses `'self'`, when the stored self-origin is exactly what preserves it.

  • Does a frame loaded from a normal https URL inherit its parent's Content-Security-Policy?
    No. That document has a response of its own, so its policy list is built from that response's `Content-Security-Policy` and `Content-Security-Policy-Report-Only` field lines, and is empty if it carries neither. Inheritance exists for documents created from local schemes, which have no response to carry a policy — and without it a page could escape its own policy by framing markup it fully controls.
  • A meta element carrying a Content-Security-Policy is written inside the srcdoc markup. What does it bind?
    That frame's document only. It is appended to the frame's own policy list alongside the inherited copy, so the frame then has to satisfy both. It does not reach the parent, and it cannot loosen the inherited copy, because each member of a document's policy list is enforced on its own terms.
  • Why does the inherited list store a self-origin at all?
    Because a document created from a local scheme can run in an opaque origin, where `'self'` would otherwise have nothing to match. The stored self-origin lets `'self'` in an inherited policy resolve to the origin the policy came from. It does not make the document's own origin any less opaque.

House rules photocopied and pinned inside a room you built yourself. The copy is taken the moment the room exists, and later edits to the original never reach it.

saying these in an interview costs you the question

  • Says a srcdoc frame starts with no policy at all
  • Claims every iframe inherits its parent's policy
  • Thinks changing the parent's policy later updates an existing frame
  • Assumes 'self' can never resolve inside an inherited policy
  • Believes a meta policy inside the frame also binds the parent
  • Treats inheritance as a live link rather than a copy