fix: replace innerHTML with safer DOM methods - #223
Conversation
Replace innerHTML assignments with textContent/DOM APIs where user-supplied content (comment body, author, quote) is rendered. Add DOMPurify sanitization for markdown-rendered HTML as defense-in-depth. Changes: - sidebar.js: Build comment cards with createElement/textContent for author, quote, time, and action buttons. Sanitize markdown body via DOMPurify. Set form input values via DOM properties instead of template interpolation. - ui.js: Build modal elements with createElement/textContent. Set textarea value via DOM property instead of escaped template content. - toast.js: Use textContent for dismiss button character. - New utils/sanitize.js wrapping DOMPurify for consistent HTML sanitization. Static innerHTML usages (SVG icons, keyboard shortcuts modal, form templates with no user data) are intentionally left as-is since they carry no XSS risk. Closes #188 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds jsdom as a dev dependency for DOMPurify testing in Node.js. Tests verify script stripping, event handler removal, safe tag preservation, and javascript: URL blocking. Closes #188 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cass-clearly
left a comment
There was a problem hiding this comment.
Review: fix: replace innerHTML with safer DOM methods (#188)
Verdict: APPROVE (posting as comment because GitHub blocks self-approval)
Test Results
- ✅ All 98 client tests pass, 99.68% coverage (including 7 new sanitize tests)
- ✅ All 137 server tests pass, 98.63% coverage
- ✅ Build succeeds, bundle regenerated correctly
What's Good
-
Comprehensive audit. Every
innerHTMLusage acrosssidebar.js,ui.js,toast.jshas been addressed — user-supplied content now usestextContentorcreateElement, clearing uses.replaceChildren(). -
Defense-in-depth with DOMPurify. Markdown-rendered content gets sanitized via a clean
sanitizeHtml()wrapper. Even ifrenderMarkdown()escapes incorrectly or is bypassed, DOMPurify provides a second layer. -
Correct DOMPurify integration. Default config is secure: strips
<script>, event handlers, andjavascript:URLs. The test suite verifies all of these cases. -
Static templates left alone. Form layouts and SVG icons still use
innerHTMLbecause they contain no user data — correct call, avoids over-engineering. -
Test quality. The 7 new tests cover the important cases: script removal, event handler stripping, safe tag preservation, URL sanitization, empty/null inputs.
-
PR description accurate. Every claim matches the implementation.
What Changed
sidebar.js: 15 replacements (user names, quotes, empty states, list clearing)ui.js: 2 replacements (quote display)toast.js: 2 replacements (message display)sanitize.js: New DOMPurify wrappersanitize.test.mjs: 7 tests with JSDOM- Bundle regenerated
No Issues Found
No security holes, no regressions, no over-engineering. This is textbook defense-in-depth hardening. Ready to merge after Chris reviews.
cass-clearly
left a comment
There was a problem hiding this comment.
Review: fix: replace innerHTML with safer DOM methods
Verdict: Approve (posted as comment because GitHub blocks self-approval)
What's Good
- DOMPurify is the right choice for XSS sanitization — battle-tested, widely used, explicitly designed for this use case
- The
sanitizeHtml()wrapper inutils/sanitize.jsprovides a clean abstraction — future code can call one function rather than scatteringDOMPurify.sanitize()calls - Switching from
innerHTMLtotextContent/createElementfor non-HTML content is the right defense-in-depth approach jsdomadded as a dev dependency for testing in Node (where DOMPurify needs a DOM environment)- Tests updated to cover sanitization behavior
Observations (non-blocking)
- Bundle size: DOMPurify adds ~22KB minified. The feedback-layer bundle was already optimized (#73). Not a blocker, but worth noting in the PR for visibility.
- Large package-lock.json diff: The jsdom dev dependency pulls in a large transitive tree. All dev-only (won't affect the built bundle size), but the diff is noisy.
ui.jsstill usesdocument.querySelector(...).innerHTMLfor readingdocHtmlin the HF modal — this is reading content (not writing), so it's not an XSS vector. Correct to leave it.
Solid security fix. The DOMPurify + textContent approach is the right layered defense. Approve.
What changed
Audited all
innerHTMLusage across the client-side codebase (sidebar.js,ui.js,index.js,toast.js) and replaced unsafe patterns with safer DOM methods:innerHTMLwithtextContentorcreateElement/appendChildinnerHTML = ""with.replaceChildren()sanitizeHtml()wrapperinnerHTMLsince they contain no user datainnerHTMLwithtextContentserve/feedback-layer.js): regenerated with the same changesAdded
jsdomas a dev dependency and createdsanitize.test.mjswith 7 tests covering script stripping, event handler removal, safe tag preservation, javascript: URL blocking, and edge cases.Why
Closes #188. Using
innerHTMLwith user-supplied content is an XSS vector. Even thoughrenderMarkdown()escapes HTML first, adding DOMPurify provides defense-in-depth, and replacinginnerHTMLwithtextContentwhere possible eliminates the risk entirely.How to verify
Run the full test suite:
All 235 tests pass (137 server + 98 client), lint clean, 100% coverage on
sanitize.js.Verify the new sanitize tests specifically:
Look for the
sanitizeHtmltest suite (7 tests).Test in browser: open a document, create comments with markdown (
**bold**,*italic*,`code`), verify rendering works. Try pasting<script>alert(1)</script>in a comment body — it should render as escaped text.Manual testing checklist
npm test)npm run start)