diff --git a/docs/design.md b/docs/design.md
index 77b0a18..fb71957 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -131,7 +131,8 @@ there.
- An opaque-origin `srcdoc` document carrying its own `` CSP
(`default-src 'none'`; `script-src` the loader's hash plus
`'wasm-unsafe-eval'`; `style-src`/`img-src`/`font-src`/`media-src
- blob:` — the asset stylesheet is a `blob:`). CSP
+ blob:` — the asset stylesheet is a `blob:`; `img-src` also `data:`, for
+ the stylesheet's inline SVG backgrounds, which fetch nothing). CSP
policies compose with the embedder's header policy, so the frame is
network-dead regardless. `sandbox="allow-scripts allow-forms"`;
`form-action 'none'`. The loader is a constant; everything variable
diff --git a/e2e/run.ts b/e2e/run.ts
index e4adc75..2fa4efd 100644
--- a/e2e/run.ts
+++ b/e2e/run.ts
@@ -1260,6 +1260,22 @@ const scenarios: Scenario[] = [
const background = await todoFrame(page).locator("section.todoapp")
.evaluate((el) => getComputedStyle(el).backgroundColor);
eq(background, "rgb(255, 255, 255)", "the stylesheet did not apply");
+
+ // The checkboxes are `data:` SVG background images in that stylesheet
+ // (todomvc-app.css `.toggle + label`), which `img-src` must admit. A
+ // computed `background-image` reads the same whether or not CSP let
+ // the image load, so load a `data:` image in the frame and see.
+ const dataImageLoads = await todoFrame(page).locator("body").evaluate(
+ () =>
+ new Promise((resolve) => {
+ const img = new Image();
+ img.onload = () => resolve(true);
+ img.onerror = () => resolve(false);
+ img.src =
+ "data:image/svg+xml,";
+ }),
+ );
+ check(dataImageLoads, "the frame's CSP blocks data: images");
},
},
diff --git a/web/boot.ts b/web/boot.ts
index 331a936..73c7f31 100644
--- a/web/boot.ts
+++ b/web/boot.ts
@@ -343,7 +343,12 @@ function base64(bytes: ArrayBuffer): string {
* cannot reach the network at all. Everything it renders comes down the
* mutation stream or out of a `blob:` the parent minted, which is why
* `style-src` allows `blob:` (the app bundle's stylesheet is an asset, so
- * `` resolves to a blob URL).
+ * `` resolves to a blob URL). `img-src` also allows `data:`: the
+ * bytes of a `data:` URL are inline in whatever names it — here, the
+ * stylesheet's `background-image` SVGs — so nothing leaves the frame. An
+ * `` from the app is still refused, by the receiver
+ * policy (web/policy.ts: URL-kind attributes take asset handles only), not
+ * by this CSP.
*
* The hash covers the exact text between the tags INCLUDING the two
* newlines, because that is what the browser hashes.
@@ -358,7 +363,7 @@ async function frameSrcdoc(frameJs: string): Promise {
"default-src 'none'",
`script-src 'sha256-${base64(digest)}' 'wasm-unsafe-eval'`,
"style-src blob:",
- "img-src blob:",
+ "img-src blob: data:",
"font-src blob:",
"media-src blob:",
"form-action 'none'",