// ============================================================ // Contract test for the Notes reader sanitizer. Confirms that // DOMPurify (loaded via jsdom here) strips the payloads we care // about — script tags, inline event handlers, iframes, object // tags — while preserving the formatting tags we DO allow. // // Why this test exists: // The original Notes module used a homegrown walker-based // sanitizer. When the body round-trips through localStorage or // a compromised row, a bypass in the sanitizer means session // theft. We swapped to DOMPurify; this test holds the contract // so a regression is loud. // ============================================================ const { test } = require('node:test'); const assert = require('node:assert/strict'); const { JSDOM } = require('jsdom'); const createDOMPurify = require('dompurify'); // Match the allowlist used in public/js/notes.js `sanitizeHtml`. const ALLOWED_TAGS = ['p','br','strong','em','b','i','u','s','h2','h3','h4', 'ul','ol','li','a','blockquote','code','pre','hr']; const ALLOWED_ATTR = ['href','target','rel']; function makeSanitizer() { const window = new JSDOM('').window; const DOMPurify = createDOMPurify(window); return function sanitize(html) { return DOMPurify.sanitize(html, { ALLOWED_TAGS, ALLOWED_ATTR, FORBID_ATTR: ['style','onerror','onload','onclick','onmouseover'], ALLOW_DATA_ATTR: false, }); }; } test('strips '); assert.ok(!out.includes('ok<\/p>/); }); test('strips inline event handlers on otherwise-allowed tags', () => { const sanitize = makeSanitizer(); const out = sanitize('link'); assert.ok(!out.includes('onclick')); assert.ok(!out.includes('steal')); assert.match(out, /href="https:\/\/x"/); }); test('drops — img is not in the allowlist', () => { const sanitize = makeSanitizer(); const out = sanitize(''); assert.ok(!out.includes(' and (framing/plugin injection)', () => { const sanitize = makeSanitizer(); assert.ok(!sanitize('').includes('iframe')); assert.ok(!sanitize('').includes('object')); }); test('drops style attributes — CSS injection / expression() on older browsers', () => { const sanitize = makeSanitizer(); const out = sanitize('

text

'); assert.ok(!out.includes('style')); assert.match(out, /

text<\/p>/); }); test('preserves every tag in the allowlist', () => { const sanitize = makeSanitizer(); for (const tag of ALLOWED_TAGS) { const html = `<${tag}>x`; const out = sanitize(html); //
and


are self-closing; DOMPurify may emit them either way. if (tag === 'br' || tag === 'hr') { assert.match(out, new RegExp(`<${tag}`), `expected <${tag}> preserved`); } else { assert.match(out, new RegExp(`<${tag}>x`), `expected <${tag}>x preserved`); } } }); test('preserves mailto + https links; drops javascript: URIs', () => { const sanitize = makeSanitizer(); assert.match(sanitize('x'), /href="https:\/\/ok"/); assert.match(sanitize('x'), /href="mailto:a@b\.c"/); const jsAttempt = sanitize('x'); assert.ok(!jsAttempt.includes('javascript:'), 'javascript: URI should be stripped'); }); test('empty + null + non-string input does not throw', () => { const sanitize = makeSanitizer(); assert.equal(sanitize(''), ''); assert.equal(sanitize(null), ''); assert.equal(sanitize(undefined), ''); }); test('nested tags: still safe

'); assert.ok(!out.includes('