# PhantomStream Security This document is the embed security contract for PhantomStream's capture -> wire -> render pipeline. It describes the always-on protections implemented in `src/capture/index.js`, `src/renderer/sanitize.js`, `src/renderer/diff.js`, `src/renderer/snapshot.js`, and `src/renderer/index.js`, plus the responsibilities a host must preserve when embedding the viewer. ## 1. Threat Model The page being mirrored is attacker-influenced input. Its DOM, attributes, CSS, text, links, forms, dialogs, and overlays can all reach the capture side. PhantomStream's job is to preserve benign visual fidelity while making mirrored content safe to render in an embedded viewer and while ensuring configured private content never leaves the captured page. | Pattern | Primary risk | Mitigation layers | |---|---|---| | Inline event handlers (`onclick`, `onload`, namespaced handler attrs) | Script execution in the mirror | Capture `sanitizeForWire`, render `sanitizeFragment` / `sanitizeAttrValue`, no `allow-scripts` sandbox | | `javascript:`, `vbscript:`, `data:text/html` URLs | Script or HTML navigation | Capture and render URL scheme blocklist; CSP `default-src 'none'` | | Nested `srcdoc` attacker iframes | Reintroduced document execution surface | `srcdoc` attributes dropped at both chokepoints | | `` / `` plugin shells | Plugin or nested resource execution | Dropped entirely at both chokepoints | | Namespace-confusion mXSS (`svg`, `math`, `xlink:href`, style breakouts) | Parser mutation turns inert-looking markup active | DOM-fragment render sanitization, namespace-aware attr scrubs, CSS breakout scrub | | CSS vectors (`expression()`, `-moz-binding`, `url(javascript:)`, hostile `@import`) | Legacy script or unwanted fetch path | Targeted CSS value scrub at capture and render | | CSSOM `styleSources[]` and `DIFF_OP.STYLE_SOURCE` ops | Stylesheet text becomes a new insertion surface | Capture-side CSS scrub, render-side `scrubCssText`, CSP, and no `allow-scripts` sandbox | | Password and PII leakage | Private text leaves the page | Capture-side masking before transport; password masking is non-configurable | | Shadow root, same-origin frame, or subtree recovery HTML | New HTML insertion surface in the mirror | Same `sanitizeForWire` capture chokepoint, render-side `sanitizeFragment`, CSP meta, and no-`allow-scripts` sandbox | | Cross-origin iframe content | Browser-origin data leakage | Capture never reads cross-origin iframe content; renderer shows content-free placeholders only | | Live value diffs | Typed values bypass snapshot masking | `DIFF_OP.VALUE` payloads route through capture-side value masking before transport | | Future serialization path bypass | A new writer skips sanitization or masking | Static scan in `tests/security-chokepoint-purity.test.js` | | New renderer `innerHTML` sink or sandbox weakening | Render-side defense bypass | InnerHTML allowlist and `allow-scripts` forbidden scan | | Security contract rot | Docs stop matching shipped controls | `docs/SECURITY.md` marker guard in the purity test | ## 2. Defense-In-Depth Pipeline PhantomStream uses five layers in order. No layer is optional, and there is no opt-out config on either the capture or renderer side. 1. **Capture chokepoint: `sanitizeForWire` (`src/capture/index.js`)** - every snapshot, add-op subtree, shadow root sidecar, same-origin frame payload, subtree response, attr op, text op, value diff, head inline style value, CSSOM `styleSources[]` entry, and `DIFF_OP.STYLE_SOURCE` op routes through this named function before `transport.send`. It strips, neutralizes, masks, or drops content only on detached clones and wire values; the live page is not mutated. 2. **Wire** - protocol messages carry already-sanitized and already-masked values. The D7 differential ledger entry documents the intentional divergence from the raw reference stream. 3. **Render chokepoints: `sanitizeFragment` and `sanitizeAttrValue` (`src/renderer/sanitize.js`)** - add-op HTML is parsed in a `