From 9866707c0492a87fcaca8d9b99ba4de7f28d01b4 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 8 May 2026 01:55:04 +0000
Subject: [PATCH 1/2] =?UTF-8?q?chore(security):=20JSON-LD=20=E5=BA=8F?=
=?UTF-8?q?=E5=88=97=E5=8C=96=E7=BB=9F=E4=B8=80=E8=B5=B0=20safeJsonLdStrin?=
=?UTF-8?q?g?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新增 lib/json-ld.ts 把 JSON.stringify 输出里能闭合 攻击载荷不再出现在输出里
- 普通对象仍是合法 JSON(JSON.parse 能还原)
- < > & 都被转义
- user-generated 字段往返保真
文档:docs/SECURITY_INVARIANTS.md 新增 INV-FE-001 条目,与 backend 仓库
SECURITY.md 共用同一套不变量编号空间(前端用 INV-FE-* 前缀避免冲突)。
历史:2026-05-07 三方 CR attack chain A 起点。配合 backend 仓库
PR #26 一同收口(admin→superadmin 提权阻断 / chat 越权写阻断 / 密码迁
bcrypt / user_follows 表补建 / compose 弱密码默认值收紧)。
---
app/[locale]/docs/[...slug]/page.tsx | 5 +--
app/[locale]/u/[username]/page.tsx | 3 +-
app/layout.tsx | 5 +--
docs/SECURITY_INVARIANTS.md | 46 +++++++++++++++++++++++++
lib/json-ld.ts | 26 +++++++++++++++
tests/json-ld.test.ts | 50 ++++++++++++++++++++++++++++
6 files changed, 130 insertions(+), 5 deletions(-)
create mode 100644 docs/SECURITY_INVARIANTS.md
create mode 100644 lib/json-ld.ts
create mode 100644 tests/json-ld.test.ts
diff --git a/app/[locale]/docs/[...slug]/page.tsx b/app/[locale]/docs/[...slug]/page.tsx
index 2a13a16..7ca6b3d 100644
--- a/app/[locale]/docs/[...slug]/page.tsx
+++ b/app/[locale]/docs/[...slug]/page.tsx
@@ -1,4 +1,5 @@
import { source } from "@/lib/source";
+import { safeJsonLdString } from "@/lib/json-ld";
import { SITE_URL } from "@/lib/site-url";
import { DocsPage, DocsBody } from "fumadocs-ui/page";
import { notFound } from "next/navigation";
@@ -104,12 +105,12 @@ export default async function DocPage({ params }: Param) {
diff --git a/app/[locale]/u/[username]/page.tsx b/app/[locale]/u/[username]/page.tsx
index 2397ca0..04b496c 100644
--- a/app/[locale]/u/[username]/page.tsx
+++ b/app/[locale]/u/[username]/page.tsx
@@ -17,6 +17,7 @@ import { GithubRepos, GithubReposSkeleton } from "./GithubRepos";
import { Suspense } from "react";
import { getTranslations } from "next-intl/server";
import { sanitizeExternalUrl } from "@/lib/url-safety";
+import { safeJsonLdString } from "@/lib/json-ld";
import { SITE_URL } from "@/lib/site-url";
interface UserView {
@@ -367,7 +368,7 @@ export default async function UserProfilePage({ params }: Param) {
diff --git a/app/layout.tsx b/app/layout.tsx
index b8b3dfe..6815ef0 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -18,6 +18,7 @@ const geistMono = localFont({
});
import { SITE_URL } from "@/lib/site-url";
+import { safeJsonLdString } from "@/lib/json-ld";
const en_description =
"内卷地狱(Involution Hell)是一个由开发者发起的开源学习社区,专注算法、系统设计、工程实践与技术分享,帮助华人程序员高效成长,专注真实进步。Involution Hell is an open-source community empowering builders with real-world engineering.";
@@ -205,7 +206,7 @@ export default function RootLayout({
type="application/ld+json"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
- __html: JSON.stringify({
+ __html: safeJsonLdString({
"@context": "https://schema.org",
"@type": "WebSite",
name: "Involution Hell",
@@ -227,7 +228,7 @@ export default function RootLayout({
"})`
+ 输出不能包含字面 ``,必须含 ``。
+- **为什么**:`JSON.stringify` 默认不转义 `<` `>` `&`,攻击者把
+ ``
+ 写进任何 user-generated 字段(profile bio、displayName 等)即触发 stored XSS。
+ satoken 存在 localStorage 且写入非 HttpOnly cookie(跨子域 pgAdmin 的设计取舍),
+ 一次 XSS 等于完整账户接管。
+- **历史**:2026-05-07 三方 CR attack chain A 起点(详见内部报告)。
diff --git a/lib/json-ld.ts b/lib/json-ld.ts
new file mode 100644
index 0000000..2514e02
--- /dev/null
+++ b/lib/json-ld.ts
@@ -0,0 +1,26 @@
+/**
+ * 把任意对象序列化为可安全嵌入
+ * JSON.stringify 默认不转义 `<`,攻击者文本作为合法 JSON 字符串嵌入 闭合 script 块,接着把后续 `
+ *
+ * JSON.stringify 默认输出原文,浏览器看到 `` 就闭合 script block,
+ * 接着把后续 ` 不再出现在输出里", () => {
+ const payload = {
+ bio: ``,
+ };
+ const out = safeJsonLdString(payload);
+ expect(out).not.toContain("");
+ expect(out).not.toContain("载荷 with & 'quotes'` };
+ const out = safeJsonLdString(original);
+ const parsed = JSON.parse(out);
+ expect(parsed.bio).toBe(original.bio);
+ });
+});
From 3e9285e4df96c438f70902e648159f190d949862 Mon Sep 17 00:00:00 2001
From: Siz Long
Date: Fri, 8 May 2026 10:13:09 +0800
Subject: [PATCH 2/2] Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
docs/SECURITY_INVARIANTS.md | 8 ++++----
lib/json-ld.ts | 12 ++++++++++--
tests/json-ld.test.ts | 4 ++--
3 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/docs/SECURITY_INVARIANTS.md b/docs/SECURITY_INVARIANTS.md
index 2dc5729..0c12026 100644
--- a/docs/SECURITY_INVARIANTS.md
+++ b/docs/SECURITY_INVARIANTS.md
@@ -1,7 +1,7 @@
# 前端安全不变量(Security Invariants)
> 这是给维护者看的代码不变量清单。
-> 公开的 vulnerability disclosure policy 见 `frontend/SECURITY.md`。
+> 公开的 vulnerability disclosure policy 见 `SECURITY.md`。
本文档登记前端代码中**不可变更的安全保护点**。
每条不变量都对应一段 lint / 测试 / 代码模式,CI 应能捕获回归。
@@ -35,9 +35,9 @@
- 暂时通过 grep 巡查兜底:
`rg -t tsx -t ts 'dangerouslySetInnerHTML' app/ | grep -v safeJsonLdString | grep "application/ld\\+json"`
应返回 0 行。建议未来加 ESLint 自定义规则。
- - 推荐补一个单元测试:
- `safeJsonLdString({bio: ""})`
- 输出不能包含字面 ``,必须含 ``。
+ - 现有单元测试见:`tests/json-ld.test.ts`
+ 例如 `safeJsonLdString({bio: ""})`
+ 输出不能包含字面 `<` 或 ``,并且应包含转义后的 `\\u003c` 序列。
- **为什么**:`JSON.stringify` 默认不转义 `<` `>` `&`,攻击者把
``
写进任何 user-generated 字段(profile bio、displayName 等)即触发 stored XSS。
diff --git a/lib/json-ld.ts b/lib/json-ld.ts
index 2514e02..4fdd7bf 100644
--- a/lib/json-ld.ts
+++ b/lib/json-ld.ts
@@ -1,7 +1,7 @@
/**
* 把任意对象序列化为可安全嵌入 ` 就闭合 script block,
* 接着把后续 `