diff --git a/plugins/voyage/playground/voyage-playground.html b/plugins/voyage/playground/voyage-playground.html index 376711c..250fdef 100644 --- a/plugins/voyage/playground/voyage-playground.html +++ b/plugins/voyage/playground/voyage-playground.html @@ -1248,6 +1248,13 @@ playground first-run shows a complete round-trip-able artifact. // v4.3 Step 25 — strip unsafe HTML-comments before markdown-it sees them. var safeText = stripUnsafeComments(text || ''); var bodyHtml = md.render(safeText); + // v4.3 Step 1 — defense in depth: sanitize bodyHtml via DOMPurify + // (finding 1d3591d4). Applied to bodyHtml ONLY — fmHtml uses our + // own escapeHtml() on capturedFrontmatter and intentional + //
/ markup that DOMPurify would otherwise strip. + var safeBody = (typeof window !== 'undefined' && window.DOMPurify && typeof window.DOMPurify.sanitize === 'function') + ? window.DOMPurify.sanitize(bodyHtml, { USE_PROFILES: { html: true } }) + : escapeHtml(bodyHtml); // Pre-render-then-wrap for
: prepend a folded frontmatter //
block at the top if the front-matter plugin captured one. var fmHtml = ''; @@ -1255,7 +1262,7 @@ playground first-run shows a complete round-trip-able artifact. fmHtml = '
Frontmatter
' +
             escapeHtml(capturedFrontmatter) + '
'; } - return fmHtml + bodyHtml; + return fmHtml + safeBody; } function escapeHtml(s) { diff --git a/plugins/voyage/tests/playground/voyage-playground.test.mjs b/plugins/voyage/tests/playground/voyage-playground.test.mjs index 7506f0d..e815aad 100644 --- a/plugins/voyage/tests/playground/voyage-playground.test.mjs +++ b/plugins/voyage/tests/playground/voyage-playground.test.mjs @@ -483,6 +483,22 @@ test('voyage-playground.html renderArtifact strips comments before md.render (v4 assert.ok(stripIdx > 0 && stripIdx < renderIdx, 'stripUnsafeComments must run before md.render'); }); +// v4.3 Step 1 — SC24-security defense in depth: renderArtifact bodyHtml is +// sanitized via DOMPurify before DOM injection (finding 1d3591d4). +test('voyage-playground.html renderArtifact sanitizes bodyHtml via DOMPurify (v4.3 Step 1, finding 1d3591d4)', () => { + const text = readFileSync(HTML, 'utf-8'); + // The literal DOMPurify.sanitize(bodyHtml expression must be present. + assert.match(text, /DOMPurify\.sanitize\(bodyHtml/, 'DOMPurify.sanitize(bodyHtml call required in renderArtifact'); + // USE_PROFILES: { html: true } must appear nearby (within the renderArtifact body) + const bodyStart = text.indexOf('function renderArtifact'); + assert.ok(bodyStart > 0, 'renderArtifact() must exist'); + const bodyEnd = text.indexOf('\n }', bodyStart); + const body = text.slice(bodyStart, bodyEnd + 1); + assert.match(body, /USE_PROFILES:\s*\{\s*html:\s*true\s*\}/, 'USE_PROFILES html:true profile required inside renderArtifact'); + // Return must reference safeBody, not raw bodyHtml + assert.match(body, /return\s+fmHtml\s*\+\s*safeBody/, 'renderArtifact return must use safeBody'); +}); + // v4.3 Step 26 — path-traversal + symlink/dotfile filter. test('voyage-playground.html declares isProjectPathSafe filter (v4.3 Step 26)', () => { const text = readFileSync(HTML, 'utf-8');