110 lines
4.2 KiB
JavaScript
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/);
|
|
});
|