Skip to content

security: client-side sanitize snapshot HTML (#110)#131

Merged
Jaggob merged 2 commits into
mainfrom
security/110-client-sanitize
Jun 7, 2026
Merged

security: client-side sanitize snapshot HTML (#110)#131
Jaggob merged 2 commits into
mainfrom
security/110-client-sanitize

Conversation

@Jaggob

@Jaggob Jaggob commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

What

Defense-in-depth for the snapshot HTML that the viewer and embed inject via innerHTML. It is already sanitized server-side by SnapshotHtmlSanitizer, but that is the sole XSS gate and .pad bodies are attacker-writable (WebDAV / a malicious share). A client-side pass adds resilience if the server gate ever regresses.

Changes

  • src/lib/sanitize-html.jssanitizeSnapshotHtml() runs DOMPurify with the same allowlist the server enforces: formatting tags only (p, br, ul, ol, li, h1–h6, strong, b, em, i, u, s, del, blockquote, pre, code), ALLOWED_ATTR: []. Mirrors SnapshotHtmlSanitizer::ALLOWED_TAGS.
  • Applied at both innerHTML sinks: embed-main.js snapshot preview and viewer-main.js renderSnapshotView.
  • Tests (tests/js/lib/sanitize-html.test.js): script/onclick/img/class stripping, disallowed-tag unwrapping, allowed-tag passthrough, nullish input. Pinned to jsdom (@vitest-environment jsdom) because happy-dom mis-sanitizes with DOMPurify.
  • Deps: dompurify (runtime) + jsdom (dev, test env only).

Acceptance

  • Snapshot HTML passes a client-side sanitizer before injection

Verification

  • vitest 123 green (7 new sanitizer cases under jsdom).
  • npm run build: DOMPurify lands in a shared chunk (~11 kB gzip) used by viewer + embed; pad-sync is bundled into the same shared chunk (Vite merged the two shared modules — functionality intact, confirmed by the literals + e2e).
  • Full Playwright suite 23 passed, 0 flaky on NC 33 — snapshot rendering (viewer + external snapshot embed) unaffected by the sanitizer.

Note on bundle cost

DOMPurify adds ~11 kB gzip to the shared Files/viewer/embed chunk. Worth it for an XSS defense layer on attacker-writable content; the issue explicitly proposed DOMPurify.

Closes #110.

Jaggob added 2 commits June 6, 2026 23:49
Snapshot HTML stored in .pad files is injected into the viewer and embed via
innerHTML. It is already sanitized server-side by SnapshotHtmlSanitizer, but
that is the sole XSS gate and .pad bodies are attacker-writable (WebDAV /
malicious share). Add a client-side defense-in-depth pass so a regression in
the server gate can't become stored XSS.

- New src/lib/sanitize-html.js: sanitizeSnapshotHtml() runs DOMPurify with the
  same allowlist the server enforces (formatting tags only, no attributes),
  mirroring SnapshotHtmlSanitizer::ALLOWED_TAGS.
- Apply it at both innerHTML sinks: embed-main.js snapshot preview and
  viewer-main.js renderSnapshotView.
- Tests: tests/js/lib/sanitize-html.test.js (pinned to jsdom — happy-dom
  mis-sanitizes with DOMPurify) covers script/onclick/img/class stripping,
  disallowed-tag unwrapping, and allowed-tag passthrough.
- Deps: dompurify (runtime, shared chunk ~11 kB gzip across viewer+embed),
  jsdom (dev, for the sanitizer test env).

vitest 123 green; full Playwright 23/23 on NC 33 (snapshot rendering
unaffected).

Closes #110.
Review follow-up on #110: embed-main computed hasSnapshotHtml from the raw
snapshotHtml, so if DOMPurify emptied it (e.g. all-dangerous markup) the embed
would still render an empty HTML preview block instead of falling back to the
snapshot text / empty message. Sanitize first, then decide the path from the
sanitized result — mirrors viewer-main's renderSnapshotView.

vitest 123 green; full Playwright 23/23 on NC 33.
@Jaggob Jaggob merged commit 2865dad into main Jun 7, 2026
11 checks passed
@Jaggob Jaggob deleted the security/110-client-sanitize branch June 17, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Defense-in-depth: client-side sanitize snapshot HTML

1 participant