#!/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',
'',
'',
'',
'Click
Hidden ',
''
].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 ',
'',
'x
y ',
'',
'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 && / {
// Phase 1.B T-S7 was revised: the framework no longer auto-sanitises plugin
// output (would break legitimate analytics ', in: '', out: '' },
{ name: 'strips ', in: '', out: '' },
{ name: 'strips (self-closing)', in: '', 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 && !/