diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template.js index ff9e4641..3f4c40f6 100644 --- a/packages/coding-agent/src/core/export-html/template.js +++ b/packages/coding-agent/src/core/export-html/template.js @@ -881,7 +881,7 @@ const images = getResultImages(); if (images.length === 0) return ''; return '
' + - images.map(img => ``).join('') + + images.map(img => ``).join('') + '
'; }; @@ -1148,7 +1148,7 @@ if (images.length > 0) { html += '
'; for (const img of images) { - html += ``; + html += ``; } html += '
'; } @@ -1491,6 +1491,32 @@ } }, renderer: { + // Sanitize link URLs to prevent javascript:/vbscript:/data: XSS + link(token) { + const href = (token.href || '').trim(); + if (/^\s*(javascript|vbscript|data):/i.test(href)) { + return this.parser.parseInline(token.tokens); + } + let out = ''; + return out; + }, + // Sanitize image src URLs + image(token) { + const href = (token.href || '').trim(); + if (/^\s*(javascript|vbscript|data):/i.test(href)) { + return escapeHtml(token.text || ''); + } + let out = '' + escapeHtml(token.text || '') + ' { + const templateJs = readFileSync(new URL("../src/core/export-html/template.js", import.meta.url), "utf-8"); + + it("overrides the marked link renderer to block javascript: protocol", () => { + // The custom link renderer must check for dangerous protocols + expect(templateJs).toMatch(/link\s*\(\s*token\s*\)/); + expect(templateJs).toMatch(/javascript/i); + expect(templateJs).toMatch(/vbscript/i); + }); + + it("overrides the marked image renderer to block javascript: protocol", () => { + expect(templateJs).toMatch(/image\s*\(\s*token\s*\)/); + }); + + it("escapes href attributes in the custom link renderer", () => { + // The link renderer must escape href values to prevent attribute breakout + expect(templateJs).toMatch(/escapeHtml\(href\)/); + }); + + it("escapes image mimeType attributes", () => { + // Image mimeType must be escaped to prevent attribute breakout + expect(templateJs).not.toMatch(/\$\{img\.mimeType\}/); + expect(templateJs).toMatch(/escapeHtml\(img\.mimeType/); + }); +});