Skip to content

Commit 1971d57

Browse files
committed
fix(web): nonce topic structured data
1 parent 50dcb92 commit 1971d57

3 files changed

Lines changed: 8 additions & 2 deletions

File tree

‎apps/web/app/routes/topic.$tid.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import { UserIdentityBadges } from "~/components/UserIdentityBadges";
6565
import { externalUrlLabel, githubProfileUrl, safeExternalUrl } from "~/lib/public-profile";
6666
import { getTopicActionPresentation } from "~/lib/topic-action-presentation";
6767
import type { TopicReplyDTO } from "~/lib/api-types";
68+
import { useCspNonce } from "~/lib/csp-nonce";
6869
import {
6970
Empty,
7071
EmptyContent,
@@ -201,10 +202,12 @@ export default function TopicDetail({ loaderData }: Route.ComponentProps) {
201202
const headings = extractMarkdownHeadings(topic.content || "").slice(0, 12);
202203
const toc = headings.length >= 4 ? <TopicToc headings={headings} /> : null;
203204
const jsonLd = discussionForumPostingJsonLd(topic);
205+
const nonce = useCspNonce();
204206

205207
return (
206208
<Layout>
207209
<script
210+
nonce={nonce}
208211
type="application/ld+json"
209212
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c") }}
210213
/>

‎apps/web/tests/SecurityHeaders.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,8 @@ describe("Web document security headers", () => {
120120
expect(rootSource).toContain("<ScrollRestoration nonce={nonce} />");
121121
expect(rootSource).toContain("<Scripts nonce={nonce} />");
122122
expect(rootSource.match(/<script\s+nonce=\{nonce\}/g)).toHaveLength(2);
123+
124+
const topicSource = readFileSync(resolve(import.meta.dirname, "../app/routes/topic.$tid.tsx"), "utf8");
125+
expect(topicSource).toContain('<script\n nonce={nonce}\n type="application/ld+json"');
123126
});
124127
});

‎openspec/changes/harden-web-security-headers/tasks.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
- [x] 2.1 实现同源 CSP 报告资源路由,仅接受规定内容类型和受限请求体,解析兼容的报告 envelope 并返回无正文响应。
1212
- [x] 2.2 对 CSP 报告使用字段白名单和 URL 脱敏,只记录 directive、去除查询参数与片段的来源摘要、同源文档路径和策略阶段;拒绝记录原始正文、Cookie、请求头或代码片段。
1313
- [x] 2.3 为报告入口接入适用的匿名限流边界,并添加有效报告、格式错误、内容类型错误、超限、跨源 URL 和敏感查询参数测试。
14-
- [ ] 2.4 在候选 CSP 中配置报告目标,并通过浏览器 smoke test 验证首页、登录、话题详情、管理入口和 Turnstile 相关页面在 Report-Only 下可渲染、hydration、导航、请求 API 和加载当前头像来源。
14+
- [x] 2.4 在候选 CSP 中配置报告目标,并通过浏览器 smoke test 验证首页、登录、话题详情、管理入口和 Turnstile 相关页面在 Report-Only 下可渲染、hydration、导航、请求 API 和加载当前头像来源。
1515

1616
## 3. Feature-Complete:部署与强制切换
1717

18-
- [ ] 3.1 核实生产 Caddy 和其他入口的现有 header 配置,为 CSP、HSTS 及其他安全头指定唯一所有者,并确认最终客户端响应不存在冲突、重复或策略放宽。
18+
- [x] 3.1 核实生产 Caddy 和其他入口的现有 header 配置,为 CSP、HSTS 及其他安全头指定唯一所有者,并确认最终客户端响应不存在冲突、重复或策略放宽。
1919
- [x] 3.2 增加无需重建镜像即可切换 CSP 阶段和 HSTS `max-age` 的安全运行时配置,校验未知值时采用安全、可用且可观测的默认行为。
2020
- [ ] 3.3 定义并执行 Report-Only 观测门槛:约定窗口内无未解释第一方违规、关键浏览器测试通过、必要来源均有评审记录,然后切换强制 CSP。
2121
- [x] 3.4 为发布后探测增加首页、交互页面和 HTML 错误响应检查,确认预期 CSP 阶段、HSTS、安全头、hydration 和资源加载,且探测输出不包含 Cookie、凭据或报告原文。

0 commit comments

Comments
 (0)