1import { describe, expect, it } from "vitest";
2import { renderIntroMarkdown } from "./markdown.js";
3
4
20
21const VECTORS = [
22
23 "<script>alert(1)</script>",
24 "<SCRIPT SRC=//evil.example/x.js></SCRIPT>",
25 "<script src='//evil.example/a.js'></script>",
26
27 "<img src=x onerror=alert(1)>",
28 "<svg/onload=alert(1)>",
29 "<body onload=alert(1)>",
30 "<input onfocus=alert(1) autofocus>",
31
32 "<iframe src=//evil.example></iframe>",
33 "<iframe src='javascript:alert(1)'></iframe>",
34 "<object data='//evil.example/x.swf'></object>",
35
36 '<a href="javascript:alert(1)">click</a>',
37 "[click](javascript:alert(1))",
38 "[click](data:text/html,<script>alert(1)</script>)",
39
40 "<scr\u0000ipt>alert(1)</script>",
41 "<script\t>alert(1)</script>",
42 "<img src=x onerror=\talert(1)>",
43
44 "<div style='width:expression(alert(1))'>",
45 "<style>body{display:none}</style>",
46
47 "<script>alert(1)</script>"
48];
49
50describe("renderIntroMarkdown — XSS sanitizer (TASK-0107 DoD)", () => {
51 it("never emits a live <script> tag for any vector", () => {
52 for (const v of VECTORS) {
53 const out = renderIntroMarkdown(v);
54 expect(out, `vector ${JSON.stringify(v)}`).not.toMatch(/<script/i);
55 }
56 });
57
58 it("never emits a live iframe/object/embed tag for any vector", () => {
59 for (const v of VECTORS) {
60 const out = renderIntroMarkdown(v);
61 expect(out, `vector ${JSON.stringify(v)}`).not.toMatch(/<(iframe|object|embed|applet)\b/i);
62 }
63 });
64
65 it("never emits a LIVE event-handler attribute (on*) for any vector", () => {
66 for (const v of VECTORS) {
67 const out = renderIntroMarkdown(v);
68
69
70
71
72 const liveTags = out.match(/<[a-zA-Z][^>]*>/g) ?? [];
73 for (const tag of liveTags) {
74 expect(tag, `live handler in ${tag} (vector ${JSON.stringify(v)})`).not.toMatch(/\bon\w+\s*=/i);
75 }
76 }
77 });
78
79 it("never emits a javascript: / data: URI in any href/src for any vector", () => {
80 for (const v of VECTORS) {
81 const out = renderIntroMarkdown(v);
82 expect(out, `vector ${JSON.stringify(v)}`).not.toMatch(/(href|src)\s*=\s*["']?\s*(javascript|data|vbscript):/i);
83 }
84 });
85
86 it("escapes raw `<` / `>` to entities (no raw tag boundary at all)", () => {
87 for (const v of VECTORS) {
88 const out = renderIntroMarkdown(v);
89
90
91 expect(out, `vector ${JSON.stringify(v)}`).not.toMatch(/<\/?(script|iframe|object|embed|img|svg|body|input|style|meta|link|form|button|textarea|select|div)\b/i);
92 }
93 });
94
95 it("the legitimate transforms still work (feature is preserved)", () => {
96
97 expect(renderIntroMarkdown("Hello **world**")).toBe("<p>Hello <strong>world</strong></p>");
98
99 expect(renderIntroMarkdown("an _italic_ word")).toBe("<p>an <em>italic</em> word</p>");
100
101 expect(renderIntroMarkdown("[docs](https://example.com/a)")).toBe(
102 '<p><a href="https://example.com/a" rel="noopener noreferrer" target="_blank">docs</a></p>',
103 );
104
105 expect(renderIntroMarkdown("- one\n- two")).toBe("<ul><li>one</li><li>two</li></ul>");
106 expect(renderIntroMarkdown("1. one\n2. two")).toBe("<ol><li>one</li><li>two</li></ol>");
107 });
108
109 it("non-http markdown links are left as literal text (no anchor)", () => {
110 const out = renderIntroMarkdown("[x](javascript:alert(1))");
111 expect(out).not.toContain("<a");
112 expect(out).toContain("javascript:alert(1)");
113 });
114
115 it("degrades gracefully on empty / non-string / over-long input (never throws)", () => {
116 expect(renderIntroMarkdown("")).toBe("");
117 expect(renderIntroMarkdown(undefined as unknown as string)).toBe("");
118 const long = "<script>".repeat(20_000);
119 expect(() => renderIntroMarkdown(long)).not.toThrow();
120 expect(renderIntroMarkdown(long)).not.toMatch(/<script/i);
121 });
122});
123