html-preview-sandbox 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). During the `0.x`
6
+ phase the public API may change between minor versions.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-07-06
11
+
12
+ Initial release.
13
+
14
+ ### Added
15
+
16
+ - Defense-in-depth preview pipeline for untrusted HTML: `decode → sanitize → CSP
17
+ policy → bridge → sandboxed iframe`.
18
+ - `createPreview(container, options)` renderer with an opaque-origin sandboxed
19
+ iframe (no `allow-same-origin`), plus lower-level `createHtmlDocument`,
20
+ `sanitizeHtml`, and `buildCsp` helpers.
21
+ - Three CSP presets layered by exfiltration capability:
22
+ - `offline` — no network at all.
23
+ - `strict` (default) — blocks attacker-readable exfiltration channels
24
+ (`connect-src 'none'`, `form-action 'none'`, no wildcard `img-src`/`media-src`)
25
+ while allowing inline script, `unsafe-eval`, and fixed CDN/font hosts.
26
+ - `balanced` — opens `https:` images/media and `connect-src https:` for
27
+ semi-trusted content.
28
+ - DOMPurify-backed sanitizer with project hooks for URL-scheme filtering, meta
29
+ refresh / user CSP removal, base-tag removal, URL-targeting SVG animation removal,
30
+ and connection/prefetch resource-hint `<link>` removal
31
+ (`preconnect`/`dns-prefetch`/`prefetch`/`prerender`), plus a removal report
32
+ surfaced through `onSanitize`.
33
+ - Injected iframe bridge forwarding link clicks, `window.open`, and
34
+ `securitypolicyviolation` reports to the host.
35
+ - Fail-closed external URL handling: protocol allowlist plus optional
36
+ `allowExternalUrl` callback; high-risk sandbox tokens filtered unless explicitly
37
+ opted in.
38
+ - Optional host navigation hook (`notifyNavigationAttempt`) that re-mounts the last
39
+ trusted document and routes the URL through the external-link allowlist.
40
+ - Separate Node (jsdom) and browser build entrypoints.
41
+ - Node and Playwright test suites, security regression fixtures, threat model,
42
+ security policy, and contributor documentation.
43
+
44
+ [Unreleased]: https://github.com/preview-sandbox/html-preview-sandbox/compare/v0.1.0...HEAD
45
+ [0.1.0]: https://github.com/preview-sandbox/html-preview-sandbox/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 html-preview-sandbox contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # html-preview-sandbox
2
+
3
+ Safely preview untrusted, interactive HTML in a sandboxed iframe.
4
+
5
+ `html-preview-sandbox` helps applications render untrusted HTML files, user uploads, and AI-generated reports without giving them full browser power. It combines DOMPurify sanitization, CSP presets, opaque-origin sandboxed iframes, external-link mediation, and host callbacks.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install html-preview-sandbox
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```js
16
+ import { createPreview } from 'html-preview-sandbox';
17
+
18
+ const preview = createPreview(document.querySelector('#preview'), {
19
+ csp: 'strict',
20
+ onOpenExternal(url) {
21
+ window.open(url, '_blank', 'noopener,noreferrer');
22
+ },
23
+ onCspViolation(report) {
24
+ console.warn('CSP blocked:', report);
25
+ },
26
+ onSanitize(report) {
27
+ console.info('Sanitize report:', report);
28
+ },
29
+ });
30
+
31
+ await preview.render(fileOrHtmlString);
32
+ ```
33
+
34
+ ## Runtime Entrypoints
35
+
36
+ The package is **ESM-only** and requires Node 18+ for the Node/default entrypoint.
37
+ It exposes separate Node and browser builds through package export conditions:
38
+
39
+ - Node/default import: uses DOMPurify with jsdom.
40
+ - Browser import: uses DOMPurify with the real browser `window`.
41
+ - Explicit browser import: `html-preview-sandbox/browser`.
42
+
43
+ Most modern bundlers resolve the `browser` condition automatically. The example and Playground run against `dist/index.browser.js`, so build the package before opening them from the repository.
44
+
45
+ Use the explicit browser subpath when your runtime or bundler does not honor the `browser` condition:
46
+
47
+ ```js
48
+ import { createPreview } from 'html-preview-sandbox/browser';
49
+ ```
50
+
51
+ ## Sanitizer Choice
52
+
53
+ This project currently uses **DOMPurify** for the sanitization layer.
54
+
55
+ - Browser runtime: DOMPurify runs against the real browser `window`.
56
+ - Node runtime: DOMPurify runs with jsdom.
57
+ - `sanitize-html` is not used in the current implementation.
58
+
59
+ DOMPurify is still only one layer. The preview also relies on CSP, a sandboxed iframe, bridge handling, external URL filtering, and optional host navigation interception.
60
+
61
+ ## What It Is
62
+
63
+ - A client-side preview pipeline for untrusted HTML files and strings.
64
+ - A way to preserve useful interactivity while reducing host-app risk.
65
+ - A small core package with Web examples and a Playground.
66
+
67
+ ## What It Is Not
68
+
69
+ - Not a browser: it does not browse URLs or support multi-page navigation.
70
+ - Not an attachment system: download, decrypt, cache, and permission checks belong to the host app.
71
+ - Not a DOMPurify replacement: sanitization is one layer in a defense-in-depth pipeline.
72
+ - Not a guarantee that all HTML is safe: see `THREAT_MODEL.md`.
73
+
74
+ ## Pipeline
75
+
76
+ ```text
77
+ input -> decode -> sanitize -> CSP policy -> bridge -> sandboxed iframe
78
+ ```
79
+
80
+ The default sandbox intentionally omits `allow-same-origin`, giving the previewed document an opaque origin. External links and `window.open` calls are forwarded to the host through callbacks. In pure Web environments, JavaScript-driven `window.location` navigation cannot be fully intercepted; Electron or other hosts can add stronger navigation controls.
81
+
82
+ Host integrations that can observe iframe navigations may call `preview.notifyNavigationAttempt(url)`. The core renderer will restore the last trusted `srcdoc` and forward the URL through the same external-link allowlist.
83
+
84
+ The three CSP presets are layered by **exfiltration capability**, not by which resources they load:
85
+
86
+ - `offline`: no network at all. Only inline scripts/styles and `data:`/`blob:` resources.
87
+ - `strict` (default): blocks attacker-readable exfiltration channels — `connect-src 'none'`, `form-action 'none'`, and no wildcard `img-src`/`media-src`. Fixed static CDN/font hosts may be loaded, and inline script plus `unsafe-eval` are permitted, because they add no attacker capability beyond the already-permitted inline scripts and provide no attacker-readable exfiltration sink. This is not "zero network" — requests to whitelisted hosts still leave the machine (see the residual-risk note in `THREAT_MODEL.md`).
88
+ - `balanced`: opens `https:` images/media and `connect-src https:`. This is a general-purpose exfiltration surface, so use it only for semi-trusted content.
89
+
90
+ Custom sandbox tokens are filtered by default. High-risk tokens such as `allow-same-origin`, `allow-downloads`, and top-navigation permissions are ignored unless `allowUnsafeSandboxTokens` is set.
91
+
92
+ External links are also fail-closed. The default protocol allowlist is `http:`, `https:`, `mailto:`, and `tel:`. Hosts can narrow it with `externalProtocols` and add domain or product rules with `allowExternalUrl`.
93
+
94
+ ## Documentation
95
+
96
+ - [Integration Guide](docs/INTEGRATION.md)
97
+ - [使用文档(中文)](docs/使用文档.zh-CN.md)
98
+ - [Security Model](docs/SECURITY_MODEL.md)
99
+ - [Sanitizer Decision](docs/SANITIZER_DECISION.md)
100
+ - [Browser Support](docs/BROWSER_SUPPORT.md)
101
+ - [Project Structure](docs/PROJECT_STRUCTURE.md)
102
+ - [Branching & Releases](docs/BRANCHING.md)
103
+ - [Roadmap](docs/ROADMAP.md)
104
+ - [Threat Model](THREAT_MODEL.md)
105
+ - [Security Policy](SECURITY.md)
106
+
107
+ ## Project Structure
108
+
109
+ The package source lives in `src/` and builds to `dist/`. Tests and local examples run against the built output so local checks match the package that consumers receive.
110
+
111
+ ```text
112
+ src/ TypeScript package source
113
+ dist/ Generated ESM bundles and declarations
114
+ playground/ Local security inspection workbench
115
+ examples/ Minimal integration examples
116
+ fixtures/ HTML regression inputs
117
+ test/ Node and Playwright tests
118
+ docs/ Integration, architecture, and security notes
119
+ ```
120
+
121
+ ## Tests
122
+
123
+ ```bash
124
+ npm run check:types
125
+ npm test
126
+ npm run test:browser
127
+ npm run build
128
+ ```
129
+
130
+ The type check validates the TypeScript source and generated public API surface. The Node test suite covers decoding, CSP generation, injection order, protocol filtering, and sanitization reports. The Playwright suite verifies real browser iframe sandboxing and external-link bridging, plus the Web example, file-upload example, Web Component, and Playground behaviors.
131
+
132
+ In CI, install the Playwright browser before `npm run test:browser`:
133
+
134
+ ```bash
135
+ npx playwright install --with-deps chrome
136
+ ```
137
+
138
+ Run the default local quality gate before publishing changes:
139
+
140
+ ```bash
141
+ npm run check
142
+ ```
143
+
144
+ ## Playground
145
+
146
+ Build the package, serve the repository root with any static file server, then open `/playground/`.
147
+
148
+ ```bash
149
+ npm run build
150
+ npm run serve
151
+ ```
152
+
153
+ Then visit `http://localhost:4173/playground/`.
154
+
155
+ The Playground is a three-panel workbench with HTML input, sandboxed preview, and an inspector for sanitizer removals, CSP violations, external requests, and current policy. It also includes sample payloads plus controls for CSP preset, external protocols, and host suffix filtering. You can drag an HTML file onto the editor, toggle a "sanitized HTML" view to inspect the exact document the pipeline produced, and use Share to encode the input + preset into a shareable URL.
156
+
157
+ ### Hosted Playground
158
+
159
+ `.github/workflows/pages.yml` deploys the Playground to GitHub Pages on every push to `main`. The workflow builds the library and assembles a site that mirrors the Playground's relative imports, so no code changes are needed. One-time setup: in the repository, go to **Settings → Pages → Source** and select **GitHub Actions**. After the first successful run the Playground is available at `https://preview-sandbox.github.io/html-preview-sandbox/playground/`.
160
+
161
+ ## Status
162
+
163
+ This is an initial v0.1 TypeScript implementation. The API is expected to evolve before 1.0.
package/SECURITY.md ADDED
@@ -0,0 +1,40 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ During the v0.x phase, security reports are accepted for the latest published minor version.
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ Please do not open public issues for security problems. Use GitHub Security Advisories when the repository is published, or contact the maintainer through the private channel listed in the repository profile.
10
+
11
+ ## Response Timeline
12
+
13
+ - Confirmation: best effort within 72 hours.
14
+ - Initial assessment: best effort within 7 days.
15
+ - High-severity fixes: target within 14 days when practical.
16
+ - Other fixes: target within 30 days when practical.
17
+
18
+ This project may be maintained by a small team or individual maintainers, so timelines are best effort.
19
+
20
+ ## Security Scope
21
+
22
+ Examples of in-scope issues:
23
+
24
+ - Sanitizer bypasses that defeat the documented default policy.
25
+ - CSP injection or bridge injection failures.
26
+ - External-link protocol validation bypasses.
27
+ - Sandbox configuration mistakes in the default renderer.
28
+ - Bridge behavior that lets untrusted HTML attack the host page.
29
+
30
+ ## Out of Scope
31
+
32
+ - CPU exhaustion, infinite loops, or mining inside the sandbox.
33
+ - Phishing content rendered as normal HTML.
34
+ - Browser, Electron, or operating-system sandbox vulnerabilities.
35
+ - Issues that require intentionally unsafe custom policy overrides.
36
+ - Self-XSS and attacks requiring users to paste trusted secrets into untrusted content.
37
+
38
+ ## Credit
39
+
40
+ Reporters may be credited in advisories and release notes unless they request anonymity.
@@ -0,0 +1,51 @@
1
+ # Threat Model
2
+
3
+ `html-preview-sandbox` is designed for apps that need to preview untrusted HTML files or strings while preserving useful interactivity.
4
+
5
+ ## Trust Boundaries
6
+
7
+ - Host app: trusted application code, storage, cookies, and user context.
8
+ - Preview iframe: untrusted HTML running in a sandboxed `srcdoc` iframe.
9
+ - External browser/system handler: destination for host-approved external links.
10
+
11
+ ## Defense Goals
12
+
13
+ - Reduce access from previewed HTML to host app DOM, storage, cookies, and APIs.
14
+ - Block common dangerous tags, attributes, protocols, and auto-triggering handlers.
15
+ - Limit data exfiltration through CSP presets.
16
+ - Prevent external links and popups from silently taking over the preview iframe.
17
+ - Surface sanitizer changes and CSP violations to the host app.
18
+
19
+ ## Non Goals
20
+
21
+ - It does not stop CPU-heavy scripts, infinite loops, or resource exhaustion inside the sandbox.
22
+ - It does not detect phishing content or misleading text rendered inside HTML.
23
+ - It does not protect against browser or runtime sandbox vulnerabilities.
24
+ - It does not make dangerous custom policies safe.
25
+ - It does not fully intercept `window.location` navigation in pure Web hosts.
26
+
27
+ ## Residual Risk: Low-Bandwidth Side Channels (strict preset)
28
+
29
+ The `strict` preset blocks general-purpose, attacker-readable exfiltration channels, but it is not "zero network". Because it permits loading from fixed static CDN and font hosts, some low-bandwidth information leakage remains possible and is **out of scope**:
30
+
31
+ - Request patterns to whitelisted hosts (for example, encoding data into a path served by a CDN whose access logs the attacker can observe indirectly).
32
+ - DNS resolution of whitelisted hostnames.
33
+ - Request timing and ordering.
34
+
35
+ These channels are narrow, aggregated, and high-latency compared to `connect-src`/image-URL exfiltration, which `strict` does block. Integrations that must eliminate even these should use the `offline` preset (no network at all).
36
+
37
+ ## Web vs Host Adapters
38
+
39
+ Pure Web integrations can intercept link clicks and `window.open` through the bridge script, but cannot reliably intercept all JavaScript-driven `window.location` navigation. Desktop hosts such as Electron can add navigation interception outside the iframe and re-render the original content.
40
+
41
+ ## Default Position
42
+
43
+ The default `strict` preset fails closed for attacker-readable exfiltration: `connect-src 'none'`, `form-action 'none'`, no wildcard `img-src`/`media-src`, and no external link opening when `onOpenExternal` is not provided. It permits inline script, `unsafe-eval`, and fixed static CDN/font hosts, none of which add attacker capability beyond the already-permitted inline scripts (see the residual-risk note above for the narrow side channels that remain).
44
+
45
+ Custom sandbox tokens are also fail-closed for high-risk permissions. `allow-same-origin`, download, and top-navigation permissions are filtered unless the host explicitly opts into unsafe sandbox tokens.
46
+
47
+ External URL handling is fail-closed in two stages: the protocol must pass the configured allowlist, then `allowExternalUrl` must return `true` when that callback is provided.
48
+
49
+ ## Sanitizer Position
50
+
51
+ The project uses DOMPurify for sanitization. DOMPurify reduces common HTML/script injection risk, but this project does not treat sanitization as sufficient by itself. CSP, iframe sandboxing, URL filtering, and host integration remain required parts of the design.
@@ -0,0 +1,3 @@
1
+ export declare const BRIDGE_OPEN_EXTERNAL = "html-preview-sandbox:openExternal";
2
+ export declare const BRIDGE_CSP_VIOLATION = "html-preview-sandbox:cspViolation";
3
+ export declare function injectBridgeScript(html: string): string;
@@ -0,0 +1,12 @@
1
+ import type { PreviewInput } from './types.js';
2
+ export declare const DEFAULT_MAX_BYTES: number;
3
+ export interface DecodeResult {
4
+ html: string;
5
+ encoding: string;
6
+ usedBom: boolean;
7
+ size: number;
8
+ }
9
+ export declare function normalizeInput(input: PreviewInput, options?: {
10
+ maxBytes?: number;
11
+ }): Promise<DecodeResult>;
12
+ export declare function decodeHtmlBytes(input: ArrayBuffer | Uint8Array): DecodeResult;
@@ -0,0 +1,2 @@
1
+ import type { PreviewInput, PreviewOptions, RenderResult } from './types.js';
2
+ export declare function createHtmlDocument(input: PreviewInput, options?: PreviewOptions): Promise<RenderResult>;
@@ -0,0 +1,2 @@
1
+ import type { PreviewInput, PreviewOptions, RenderResult } from './types.js';
2
+ export declare function createHtmlDocument(input: PreviewInput, options?: PreviewOptions): Promise<RenderResult>;
@@ -0,0 +1,12 @@
1
+ import type { PreviewErrorCode } from './types.js';
2
+ export declare class PreviewError extends Error {
3
+ code: PreviewErrorCode;
4
+ cause?: unknown;
5
+ constructor(code: PreviewErrorCode, message: string, cause?: unknown);
6
+ }
7
+ export declare const ERROR_CODES: {
8
+ readonly OVERSIZED: "OVERSIZED";
9
+ readonly DECODE_FAILED: "DECODE_FAILED";
10
+ readonly EMPTY_AFTER_SANITIZE: "EMPTY_AFTER_SANITIZE";
11
+ readonly RENDER_FAILED: "RENDER_FAILED";
12
+ };
@@ -0,0 +1,9 @@
1
+ export type { CspPolicy, CspPreset, CspViolationReport, OpenExternalSource, PreviewErrorCode, PreviewErrorShape, PreviewHandle, PreviewInput, PreviewOptions, RenderResult, SanitizeOptions, SanitizeReport, } from './types.js';
2
+ export { createPreview } from './renderer.browser.js';
3
+ export { createHtmlDocument } from './document.browser.js';
4
+ export { decodeHtmlBytes, normalizeInput, DEFAULT_MAX_BYTES } from './decode.js';
5
+ export { sanitizeHtml } from './sanitize.browser.js';
6
+ export { DEFAULT_ALLOWED_EXTERNAL_PROTOCOLS, DEFAULT_SANDBOX_TOKENS, UNSAFE_SANDBOX_TOKENS, buildCsp, getSandboxAttribute, injectCspMeta, isAllowedExternalUrl, } from './policy.js';
7
+ export { hasAuthorScrollbarStyle, injectScrollbarStyle, } from './scrollbar.js';
8
+ export { BRIDGE_CSP_VIOLATION, BRIDGE_OPEN_EXTERNAL, injectBridgeScript, } from './bridge.js';
9
+ export { PreviewError, ERROR_CODES } from './errors.js';