pediatric-ai-scribe-v3/test/notes-sanitize.test.js
2026-05-08 06:28:12 +02:00

110 lines
4.2 KiB
JavaScript

// ============================================================
// 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/utils.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 <script> tags completely', () => {
const sanitize = makeSanitizer();
const out = sanitize('<p>ok</p><script>alert(1)</script>');
assert.ok(!out.includes('<script'));
assert.ok(!out.includes('alert'));
assert.match(out, /<p>ok<\/p>/);
});
test('strips inline event handlers on otherwise-allowed tags', () => {
const sanitize = makeSanitizer();
const out = sanitize('<a href="https://x" onclick="steal()">link</a>');
assert.ok(!out.includes('onclick'));
assert.ok(!out.includes('steal'));
assert.match(out, /href="https:\/\/x"/);
});
test('drops <img onerror="..."> — img is not in the allowlist', () => {
const sanitize = makeSanitizer();
const out = sanitize('<img src=x onerror="alert(1)">');
assert.ok(!out.includes('<img'));
assert.ok(!out.includes('onerror'));
});
test('drops <iframe> and <object> (framing/plugin injection)', () => {
const sanitize = makeSanitizer();
assert.ok(!sanitize('<iframe src="evil"></iframe>').includes('iframe'));
assert.ok(!sanitize('<object data="evil"></object>').includes('object'));
});
test('drops style attributes — CSS injection / expression() on older browsers', () => {
const sanitize = makeSanitizer();
const out = sanitize('<p style="background:url(evil)">text</p>');
assert.ok(!out.includes('style'));
assert.match(out, /<p>text<\/p>/);
});
test('preserves every tag in the allowlist', () => {
const sanitize = makeSanitizer();
for (const tag of ALLOWED_TAGS) {
const html = `<${tag}>x</${tag}>`;
const out = sanitize(html);
// <br> and <hr> 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</${tag}>`), `expected <${tag}>x</${tag}> preserved`);
}
}
});
test('preserves mailto + https links; drops javascript: URIs', () => {
const sanitize = makeSanitizer();
assert.match(sanitize('<a href="https://ok">x</a>'), /href="https:\/\/ok"/);
assert.match(sanitize('<a href="mailto:a@b.c">x</a>'), /href="mailto:a@b\.c"/);
const jsAttempt = sanitize('<a href="javascript:alert(1)">x</a>');
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: <script> inside <p> still stripped', () => {
const sanitize = makeSanitizer();
const out = sanitize('<p>safe<script>evil()</script>still safe</p>');
assert.ok(!out.includes('<script'));
assert.ok(!out.includes('evil()'));
// Text content before + after the stripped tag should survive
assert.match(out, /safe/);
assert.match(out, /still safe/);
});