#!/usr/bin/env node /** * docmd Security Test Suite * ========================== * End-to-end tests for the Phase 0 security primitives: * - security.html = 'escape' (default) | 'allow' | 'strip' * - escape helpers (escHtml / attrEsc / jsonInject / scriptLiteral) integration * - safePath() path-traversal guard * * Run: node tests/security.test.js */ import { execSync, spawn, spawnSync } from 'child_process'; import fs from 'fs'; import path from 'path'; const DOCMD = path.resolve(import.meta.dirname, '../packages/core/dist/bin/docmd.js'); const TEST_ROOT = '/tmp/docmd-brute-security'; const PASS = '✅'; const FAIL = '❌'; let passed = 0; let failed = 0; const failures = []; function setup(name) { const dir = path.join(TEST_ROOT, name); fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(dir, { recursive: true }); return dir; } function build(dir) { // Use spawnSync so we can capture both stdout AND stderr on success too // (execSync discards stderr on success). Plugins use console.error to // surface validation warnings (e.g. S-4 analytics, S-5 PWA). const r = spawnSync('node', [DOCMD, 'build'], { cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); return { ok: r.status === 0, output: r.stdout || '', stderr: r.stderr || '' }; } function writeFile(dir, filePath, content) { const full = path.join(dir, filePath); fs.mkdirSync(path.dirname(full), { recursive: true }); fs.writeFileSync(full, content); } function readSite(dir, filePath) { const full = path.join(dir, 'site', filePath); if (!fs.existsSync(full)) return null; return fs.readFileSync(full, 'utf8'); } function assert(testName, condition, detail = '') { if (condition) { console.log(` ${PASS} ${testName}`); passed++; } else { console.log(` ${FAIL} ${testName}${detail ? ': ' + detail : ''}`); failed++; failures.push(testName); } } // ─── TEST S1: Default security.html = 'escape' blocks raw HTML ────────── console.log('\n🔒 Test S1: Default html policy is escape (Phase 0.D)'); { const dir = setup('s1-default-escape'); writeFile(dir, 'docs/index.md', [ '---', 'title: Raw HTML Test', '---', '', '# Heading', '', '', '', '
ClickHidden
', '' ].join('\n')); const r = build(dir); assert('build succeeds', r.ok, r.output); const html = readSite(dir, 'index.html'); assert('output exists', html !== null); assert('default policy escapes the user canary', html && !html.includes('alert("xss")')); assert('default policy escapes
', html && !html.includes('
')); assert('escaped output shows <script>', html && html.includes('<script>')); } // ─── TEST S2: Explicit 'allow' policy passes raw HTML through ─────────── console.log('\n🔒 Test S2: security.html = "allow" passes raw HTML'); { const dir = setup('s2-allow'); writeFile(dir, 'docmd.config.json', JSON.stringify({ title: 'Allow', security: { html: 'allow' } })); writeFile(dir, 'docs/index.md', [ '# Heading', '', '', '' ].join('\n')); const r = build(dir); assert('build succeeds', r.ok, r.output); const html = readSite(dir, 'index.html'); assert('allow policy keeps ', '', '
xy
', '', 'after' ].join('\n')); const r = build(dir); assert('build succeeds', r.ok, r.output); const html = readSite(dir, 'index.html'); assert('strip policy removes the user canary', html && !html.includes('alert("xss")')); assert('strip policy removes
', html && !html.includes('
')); assert('strip policy removes
', html && !html.includes('
')); assert('strip policy keeps surrounding text', html && html.includes('before') && html.includes('after')); } // ─── TEST S4: Invalid policy value falls back to 'escape' ──────────────── console.log('\n🔒 Test S4: Invalid policy value defaults to escape'); { const dir = setup('s4-invalid-policy'); writeFile(dir, 'docmd.config.json', JSON.stringify({ title: 'Invalid', security: { html: 'bogus' } })); writeFile(dir, 'docs/index.md', '# Hi\n\n\n'); const r = build(dir); assert('build succeeds with bogus policy', r.ok, r.output); const html = readSite(dir, 'index.html'); assert('bogus value falls back to escape', html && !html.includes('\n'); const r = build(dir); assert('build succeeds with no security block', r.ok, r.output); const html = readSite(dir, 'index.html'); assert('no security block falls back to escape', html && !html.includes('', '---', '', '# Hi' ].join('\n')); const r = build(dir); assert('build succeeds', r.ok, r.output); const html = readSite(dir, 'index.html'); assert('output exists', html !== null); // The frontmatter title is the malicious payload; the site title is benign. // We assert that the malicious payload is HTML-escaped in the meta tags. assert('og:title escapes the frontmatter ', html && /', out: '' }, { name: 'neutralises javascript: href', in: 'x', out: 'x' }, { name: 'neutralises vbscript: href', in: 'x', out: 'x' }, { name: 'leaves safe alone', in: 'x', out: 'x' }, { name: 'leaves safe alone', in: '', out: '' }, { name: 'handles mixed content', in: '

safe

', out: '

safe

' } ]; for (const c of cases) { const actual = sanitizeHeadInjection(c.in); assert( c.name, actual === c.out, `input=${JSON.stringify(c.in)} actual=${JSON.stringify(actual)} expected=${JSON.stringify(c.out)}` ); } })(); // ─── TEST S16: Analytics plugin rejects invalid measurementId / trackingId (Phase 1.C, S-4) ─ console.log('\n🔒 Test S16: Analytics plugin format-validates IDs (Phase 1.C, S-4)'); { // Case 1: invalid GA4 id with XSS payload { const dir = setup('s16-analytics-bad-ga4'); writeFile(dir, 'docmd.config.json', JSON.stringify({ title: 'Bad GA4', plugins: { analytics: { googleV4: { measurementId: 'G-FAKE\'; alert(1); //' } } } })); writeFile(dir, 'docs/index.md', '# Hi\n'); const r = build(dir); assert('build succeeds with invalid GA4 id', r.ok, r.stderr || r.output); const html = readSite(dir, 'index.html'); assert('no alert(1) in built HTML', html && !/alert\(1\)/.test(html)); assert('no googletagmanager script injected', html && !/googletagmanager\.com/.test(html)); assert('error log mentions invalid id', /Invalid googleV4/.test(r.stderr || r.output)); } // Case 2: valid GA4 id should still work { const dir = setup('s16-analytics-good-ga4'); writeFile(dir, 'docmd.config.json', JSON.stringify({ title: 'Good GA4', plugins: { analytics: { googleV4: { measurementId: 'G-ABC123XYZ' } } } })); writeFile(dir, 'docs/index.md', '# Hi\n'); const r = build(dir); assert('build succeeds with valid GA4 id', r.ok, r.stderr || r.output); const html = readSite(dir, 'index.html'); assert('googletagmanager script IS injected', html && /googletagmanager\.com/.test(html)); assert('valid id appears as JSON-quoted string', html && /gtag\('config', "G-ABC123XYZ"\)/.test(html)); } // Case 3: invalid UA id { const dir = setup('s16-analytics-bad-ua'); writeFile(dir, 'docmd.config.json', JSON.stringify({ title: 'Bad UA', plugins: { analytics: { googleUA: { trackingId: 'UA-INJECTED\'); alert(1); (' } } } })); writeFile(dir, 'docs/index.md', '# Hi\n'); const r = build(dir); assert('build succeeds with invalid UA id', r.ok, r.stderr || r.output); const html = readSite(dir, 'index.html'); assert('no UA injection', html && !/google-analytics\.com\/analytics\.js/.test(html) || !/attacker/.test(html)); assert('error log mentions invalid UA', /Invalid googleUA/.test(r.stderr || r.output)); } } // ─── TEST S17: PWA plugin validates themeColor (Phase 1.C, S-5) ─ console.log('\n🔒 Test S17: PWA plugin validates themeColor (Phase 1.C, S-5)'); { // Case 1: valid hex color works { const dir = setup('s17-pwa-valid-color'); writeFile(dir, 'docmd.config.json', JSON.stringify({ title: 'PWA valid', plugins: { pwa: { themeColor: '#ff00aa' } } })); writeFile(dir, 'docs/index.md', '# Hi\n'); const r = build(dir); assert('build succeeds', r.ok, r.stderr || r.output); const html = readSite(dir, 'index.html'); assert('valid theme color preserved', html && //.test(html)); } // Case 2: invalid themeColor (XSS payload) is rejected { const dir = setup('s17-pwa-xss-color'); writeFile(dir, 'docmd.config.json', JSON.stringify({ title: 'PWA XSS', plugins: { pwa: { themeColor: '">' } } })); writeFile(dir, 'docs/index.md', '# Hi\n'); const r = build(dir); assert('build succeeds with invalid themeColor', r.ok, r.stderr || r.output); const html = readSite(dir, 'index.html'); assert('no live script from themeColor XSS', html && !/