Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions iframe-sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,12 @@ a sandboxed iframe to execute arbitrary code.
## Goals

To run untrusted code within an iframe, with the ability to communicate with
host to read, write, and subscribe to values in a key value store.

Code within an iframe **MUST NOT** be able to communicate with any third party.

For example, private data could be added to query parameters in a background
image or JS module URL.

In the future, there could be verified authors and domains per frame.
host to read, write, and subscribe to values in a key value store, not allowing
code to communicate with any external third party.

> [!CAUTION]
> During experimental development, there are currently hardcoded CDNs that are
> accessible in the iframe context. Data could be exfiltrated this way to those
> CDNs.
> During experimental development, there are intentional gaps in the sandboxing
> to enable product features where data within the sandbox may be exfiltrated.

## Usage

Expand Down Expand Up @@ -70,9 +63,11 @@ frame that propagates to the inner (untrusted) frame across browsers.

## Incomplete Security Considerations

- Currently, the hardcoded CDNs (and their logging services) **MAY** receive
exfiltrated data. We should only allow 1P mediated communications in the
future.
Some of these are shortcomings of implementation, and some are intentional
product decisisons during experimentation.

- Hardcoded CDNs (and their logging services) are an exfiltration vector.
- Allowing anchor elements with `target="_blank"` is an exfiltration vector.
- `document.baseURI` is accessible in an iframe, leaking the parent URL
- Currently without CFC, data can be written in the iframe containing other
sensitive data, or newly synthesized fingerprinting via capabilities
Expand Down
2 changes: 1 addition & 1 deletion iframe-sandbox/src/common-iframe-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ export class CommonIframeSandboxElement extends LitElement {
<iframe
${ref(this.iframeRef)}
allow="clipboard-write"
sandbox="allow-scripts allow-pointer-lock"
sandbox="allow-scripts allow-pointer-lock allow-popups allow-popups-to-escape-sandbox"
.srcdoc=${OuterFrame}
height="100%"
width="100%"
Expand Down
4 changes: 2 additions & 2 deletions iframe-sandbox/src/csp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export const CSP = `` +
`style-src ${HOST_ORIGIN} 'unsafe-inline' ${STYLE_CDNS.join(" ")};` +
// Fonts: Allow 1P, inline.
`font-src ${HOST_ORIGIN} 'unsafe-inline' ${FONT_CDNS.join(" ")};` +
// Images: Allow 1P, inline.
`img-src ${HOST_ORIGIN} 'unsafe-inline';` +
// Images: Allow 1P, data URIs.
`img-src ${HOST_ORIGIN} data:;` +
// Disabling until we have a concrete case.
`form-action 'none';` +
// Disable <base> element
Expand Down
6 changes: 3 additions & 3 deletions iframe-sandbox/src/outer-frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export default `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="${CSP}" \/>
<style>
<meta http-equiv="Content-Security-Policy" content="${CSP}" \/>
<style>
html, body {
padding: 0;
margin: 0;
Expand All @@ -30,7 +30,7 @@ iframe {
<body>
<iframe
allow="clipboard-write"
sandbox="allow-scripts allow-modals"><\/iframe>
sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-modals"><\/iframe>
<script>
const iframe = document.querySelector("iframe");
const HOST_ORIGIN = "${HOST_ORIGIN}";
Expand Down
38 changes: 38 additions & 0 deletions iframe-sandbox/test/iframe-csp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,23 @@ const SCRIPT_URL = "https://common.tools/static/sketch.js";
const STYLE_URL = "https://common.tools/static/main.css";
const IMG_URL = "https://common.tools/static/text.png";
const ORIGIN_URL = new URL(globalThis.location.href).origin;
const BASE64_IMG_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=";

function openWindow(target: any) {
return `<script>
let win = window.open("${HTML_URL}", "${target}");
if (win) throw new Error("Window Opened");</script>`;
}

function clickAnchor(target: string) {
return `
<a id="anchor-test" href="${HTML_URL}" target="${target}">
<script>
const anchor = document.querySelector("#anchor-test");
anchor.click();
</script>`;
}

const cases = [[
"allows inline script",
`<script>console.log("foo")</script><style>* { background-color: red; }</style><div>foo</div>`,
Expand All @@ -78,6 +88,10 @@ const cases = [[
"allows 1P img",
`<img src="${ORIGIN_URL}/foo.jpg" />`,
null,
], [
"allows data: img",
`<img src="${BASE64_IMG_URL}" />`,
null,
], [
"allows 1P CSS",
`<link rel="stylesheet" href="${ORIGIN_URL}/styles.css">`,
Expand All @@ -98,6 +112,18 @@ const cases = [[
"disallows opening windows (_top)",
openWindow("_top"),
null,
], [
"disallows anchor link target (_parent)",
clickAnchor("_parent"),
null,
], [
"disallows anchor link target (_self)",
clickAnchor("_self"),
null,
], [
"disallows anchor link target (_top)",
clickAnchor("_top"),
null,
], [
"disallows fetch",
`<script>fetch("${SCRIPT_URL}");</script>`,
Expand Down Expand Up @@ -161,6 +187,15 @@ const falseNegatives = [[
"CSP:default-src",
]];

// /!\ These tests do not report correctly.
// /!\ Not sure why! But they appear to be allowed
// /!\ but are not in practice.
const falsePositives = [[
"Allows anchor link target (_blank)",
clickAnchor("_blank"),
null,
]];

const unknownStatuses = [
[
// `prerender` is a Chrome-only feature-flagged
Expand All @@ -178,6 +213,9 @@ for (const [name, html, expected] of cases) {
for (const [name, html, expected] of falseNegatives) {
definePending(name, html, expected);
}
for (const [name, html, expected] of falsePositives) {
definePending(name, html, expected);
}
for (const [name, html, expected] of unknownStatuses) {
definePending(name, html, expected);
}
Expand Down