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/); }); it("escapes image data attributes", () => { // Image data is embedded in src attributes and must not allow attribute breakout. expect(templateJs).not.toMatch(/;base64,\$\{img\.data\}"/); expect(templateJs).toMatch(/;base64,\$\{escapeHtml\(img\.data \|\| (?:''|"")\)\}"/); }); it("escapes entry IDs before inserting them into attributes", () => { // Session entry IDs are embedded in id and data-entry-id attributes. expect(templateJs).not.toMatch(/id="\$\{entryId\}"/); expect(templateJs).not.toMatch(/data-entry-id="\$\{entryId\}"/); expect(templateJs).toMatch(/entry-\$\{escapeHtml\(entry\.id\)\}/); expect(templateJs).toMatch(/data-entry-id="\$\{escapeHtml\(entryId\)\}"/); }); it("escapes tree metadata rendered from session fields", () => { // The tree renders session metadata via innerHTML, so dynamic fields must be escaped. expect(templateJs).not.toMatch(/\[\$\{msg\.toolName \|\| 'tool'\}\]/); expect(templateJs).not.toMatch(/\[\$\{msg\.role\}\]/); expect(templateJs).not.toMatch(/\[model: \$\{entry\.modelId\}\]/); expect(templateJs).not.toMatch(/\[thinking: \$\{entry\.thinkingLevel\}\]/); expect(templateJs).not.toMatch(/\[\$\{entry\.type\}\]/); expect(templateJs).toMatch(/\$\{escapeHtml\(msg\.toolName \|\| 'tool'\)\}/); expect(templateJs).toMatch(/\$\{escapeHtml\(msg\.role\)\}/); expect(templateJs).toMatch(/\$\{escapeHtml\(entry\.modelId\)\}/); expect(templateJs).toMatch(/\$\{escapeHtml\(entry\.thinkingLevel\)\}/); expect(templateJs).toMatch(/\$\{escapeHtml\(entry\.type\)\}/); }); it("escapes model names in the exported header", () => { // Assistant message provider/model values are collected from the session and rendered with innerHTML. expect(templateJs).not.toMatch(/\$\{globalStats\.models\.join\(', '\) \|\| 'unknown'\}/); expect(templateJs).toMatch(/\$\{escapeHtml\(globalStats\.models\.join\(', '\) \|\| 'unknown'\)\}/); }); });