pediatric-ai-scribe-v3/test/frontend-rendering-safety.test.js
Daniel e2444066a0 feat: the announcement banner renders Markdown, safely
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
2026-09-10 19:08:12 +02:00

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, '&#39;'\)/);
});
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\(/);
});