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 '
';
};
@@ -1148,7 +1148,7 @@
if (images.length > 0) {
html += '';
for (const img of images) {
- html += `

`;
+ html += `
};base64,${img.data})
`;
}
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 = '' + this.parser.parseInline(token.tokens) + '';
+ 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 = '
';
+ return out;
+ },
// Code blocks: syntax highlight, no HTML escaping
code(token) {
const code = token.text;
diff --git a/packages/coding-agent/test/export-html-xss.test.ts b/packages/coding-agent/test/export-html-xss.test.ts
new file mode 100644
index 00000000..077a0d12
--- /dev/null
+++ b/packages/coding-agent/test/export-html-xss.test.ts
@@ -0,0 +1,28 @@
+import { readFileSync } from "fs";
+import { describe, expect, it } from "vitest";
+
+describe("export HTML markdown link sanitization", () => {
+ 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/);
+ });
+});