It was textContent, so formatting appeared literally. It now renders inline Markdown only — bold, italic, code, links, breaks — because this text is injected into every page and must not be able to introduce headings, images or block layout that shifts the app around. parseInline rather than parse, a tag allow-list, event-handler and style attributes forbidden, data attributes off, and one innerHTML assignment that can only be reached through DOMPurify. Missing either library, or any failure while rendering, falls back to the literal text rather than to unsanitised markup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
44 lines
2.5 KiB
JavaScript
44 lines
2.5 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const root = path.join(__dirname, '..');
|
|
|
|
test('admin HTML escape helper also escapes single quotes for attribute contexts', () => {
|
|
const source = fs.readFileSync(path.join(root, 'public', 'js', 'admin.js'), 'utf8');
|
|
assert.match(source, /function adminEscapeHtml\(str\)[\s\S]+replace\(\/\'\/g, '''\)/);
|
|
});
|
|
|
|
test('billing E/M suggestion fields are escaped before innerHTML insertion', () => {
|
|
const source = fs.readFileSync(path.join(root, 'public', 'js', 'app.js'), 'utf8');
|
|
assert.match(source, /Level ' \+ escHtml\(data\.emLevel\.level\)/);
|
|
assert.match(source, /MDM: ' \+ escHtml\(data\.emLevel\.complexity\)/);
|
|
assert.match(source, /escHtml\(data\.emLevel\.diagnosisCount\)/);
|
|
assert.match(source, /escHtml\(data\.emLevel\.rosCount\)/);
|
|
assert.match(source, /escHtml\(data\.emLevel\.peCount\)/);
|
|
});
|
|
|
|
test('the announcement banner formats without becoming an injection point', () => {
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const app = fs.readFileSync(path.join(__dirname, '..', 'public/js/app.js'), 'utf8');
|
|
const fn = app.slice(app.indexOf('function renderAnnouncement(el, raw)'), app.indexOf('function loadAnnouncement()'));
|
|
|
|
// Inline only: this text sits on every page, so it must not be able to
|
|
// introduce headings, images or block layout that shifts the app around.
|
|
assert.match(fn, /window\.marked\.parseInline\(markdown, \{ gfm: true \}\)/);
|
|
assert.match(fn, /ALLOWED_TAGS: \['strong', 'em', 'b', 'i', 'u', 's', 'code', 'a', 'br'\]/);
|
|
assert.doesNotMatch(fn, /'img'|'script'|'iframe'|'h1'|'h2'/);
|
|
assert.match(fn, /FORBID_ATTR: \['style', 'onerror', 'onload', 'onclick', 'onmouseover'\]/);
|
|
assert.match(fn, /ALLOW_DATA_ATTR: false/);
|
|
|
|
// Sanitising is not optional: without either library, or on any failure, the
|
|
// text is shown literally rather than as markup.
|
|
assert.match(fn, /if \(!window\.marked \|\| typeof window\.marked\.parseInline !== 'function' \|\|\s*\n\s*!window\.DOMPurify \|\| typeof window\.DOMPurify\.sanitize !== 'function'\) \{\s*\n\s*el\.textContent = markdown;/);
|
|
assert.match(fn, /catch \(e\) \{\s*\n\s*el\.textContent = markdown;/);
|
|
// innerHTML is only ever reached through sanitize().
|
|
const assignments = fn.match(/\.innerHTML\s*=/g) || [];
|
|
assert.equal(assignments.length, 1, 'one innerHTML assignment');
|
|
assert.match(fn, /el\.innerHTML = window\.DOMPurify\.sanitize\(/);
|
|
});
|