From ffad56f21bced9d8cc5408727b952d2d2b5f7cc3 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 25 May 2026 11:15:32 +0800 Subject: [PATCH] docs: add prompt test csrf initialization fix prd --- prd-prompt-test-csrf-initialization-fix.md | 507 +++++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 prd-prompt-test-csrf-initialization-fix.md diff --git a/prd-prompt-test-csrf-initialization-fix.md b/prd-prompt-test-csrf-initialization-fix.md new file mode 100644 index 0000000..db32eeb --- /dev/null +++ b/prd-prompt-test-csrf-initialization-fix.md @@ -0,0 +1,507 @@ +# Prompt 详情页测试功能安全令牌未初始化问题修复 PRD + +> **版本**: v1.0 +> **日期**: 2026-05-25 +> **状态**: 📝 待评审 + +--- + +## 一、背景 + +在 prompt 服务的公开提示词详情页中,页面提供了“测试”标签,允许已登录用户直接选择模型并运行提示词测试。 + +目标页面示例: + +- `https://prompt.ephron.ren/prompts/prompt-20260508110413` + +按预期: + +- 用户只要处于已登录状态 +- 打开公开详情页 +- 进入“测试”标签并点击“开始测试” + +就应当可以直接完成测试。 + +但现状是:即使用户已经登录,测试功能仍可能提示: + +- `安全令牌未初始化,请刷新页面后重试` +- 或在接口层表现为 `CSRF token 验证失败` + +这说明“已登录”与“测试功能可用”之间还存在一个未被详情页自身完成的初始化前置条件。 + +--- + +## 二、问题现象 + +### 2.1 用户侧现象 + +在已登录状态下,用户访问公开提示词详情页: + +1. 打开 `/prompts/{key}` 页面 +2. 切换到“测试”标签 +3. 选择模型 +4. 输入测试内容 +5. 点击“开始测试” + +**实际结果:** + +- 页面提示安全令牌未初始化,或请求失败 +- 后端接口返回 403,错误为 `CSRF token 验证失败` + +### 2.2 旁证现象 + +如果用户先访问某些后台页面(例如 `/admin/settings`),再回到同一个公开提示词详情页重复操作,测试又可能恢复正常。 + +这说明: + +- 模型能力本身不是根因 +- 登录态本身也不是根因 +- 更像是某个与页面测试功能相关的 token / cookie 初始化,只在后台页面中发生,而在公开详情页中未发生 + +--- + +## 三、影响范围 + +### 3.1 直接影响 + +- 所有挂载了“测试”面板的公开提示词详情页:`/prompts/{key}` +- 所有“已登录但首次直接从公开详情页发起测试”的用户 + +### 3.2 间接影响 + +- 用户会误以为账号失效、会话异常或模型配置有问题 +- 测试功能表现为“不稳定”:访问过后台页面后又恢复,增加排查成本 +- 页面对后台页面存在隐式依赖,破坏公开详情页的独立性 + +### 3.3 不受影响的路径 + +- 仅浏览提示词正文的用户 +- 不使用测试功能的用户 +- 已经访问过会初始化 CSRF cookie 的后台页面,且当前 cookie 尚未过期的用户 + +--- + +## 四、现状与源码定位 + +### 4.1 公开详情页会直接挂载测试组件 + +文件:`prompt/templates/public/detail.html` + +详情页模板中直接加载测试脚本并实例化 `PromptTester`: + +- 加载 `/static/js/test-prompt.js` +- `new PromptTester({...})` + +这意味着: + +- 公开详情页自身就是测试功能的承载页面 +- 测试所需前置条件应由该页面或其服务端路由负责准备 + +### 4.2 前端测试逻辑直接从 cookie 中读取 `ephron_csrf` + +文件:`prompt/static/js/test-prompt.js` + +核心逻辑: + +```javascript +const csrfToken = document.cookie.match(/ephron_csrf=([^;]+)/)?.[1] || ''; +const response = await fetch(`/api/prompts/${this.promptKey}/test`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": csrfToken, + }, + body: JSON.stringify(body), +}); +``` + +可见前端默认假设: + +- `document.cookie` 中一定已经存在 `ephron_csrf` + +若该 cookie 不存在,则会把空字符串作为 `X-CSRF-Token` 发给后端。 + +### 4.3 测试 API 后端强制校验 `ephron_auth + ephron_csrf` + +文件:`prompt/src/routes/api.py` + +关键逻辑: + +```python +token = request.cookies.get("ephron_auth") +user = get_auth_user(token) +if not user: + raise HTTPException(status_code=401, detail="需要登录") + +csrf_cookie = request.cookies.get("ephron_csrf") +csrf_header = request.headers.get("X-CSRF-Token") +if not verify_csrf_token(csrf_header, csrf_cookie): + raise HTTPException(status_code=403, detail="CSRF token 验证失败") +``` + +说明测试接口要求同时满足: + +1. 用户已登录(`ephron_auth` 有效) +2. 浏览器中存在 `ephron_csrf` +3. 请求头中的 `X-CSRF-Token` 与 cookie 中的 token 一致 + +### 4.4 后台页面会主动初始化 CSRF cookie + +文件:`prompt/src/routes/admin.py` + +后台多个 GET 页面采用统一模式: + +```python +csrf_token = generate_csrf_token() +response = templates.TemplateResponse(...) +set_csrf_cookie(response, csrf_token, is_development=IS_DEVELOPMENT) +return response +``` + +说明项目当前已有成熟的 CSRF 初始化机制: + +- 生成 token +- 写入 `ephron_csrf` cookie +- 供表单和前端 JS 后续读取 + +### 4.5 公开详情页路由未承担相同职责 + +结合现有 PRD 记录与本次源码排查,可以确认公开详情页路由: + +文件:`prompt/src/routes/pages.py` + +在渲染 `public/detail.html` 时: + +- 直接返回 `TemplateResponse` +- 没有看到对 `generate_csrf_token()` 的调用 +- 没有看到对 `set_csrf_cookie(...)` 的调用 + +这与后台页面形成明显差异。 + +--- + +## 五、根因分析 + +### 5.1 根因结论 + +> **公开提示词详情页提供了测试能力,但没有初始化该能力所依赖的 CSRF token cookie。** + +### 5.2 具体链路 + +当前链路可还原为: + +1. 用户在 `auth.ephron.ren` 完成登录 +2. 浏览器具备有效的 `ephron_auth` +3. 用户直接进入 `prompt.ephron.ren/prompts/{key}` +4. 详情页渲染时没有种下 `ephron_csrf` +5. 用户点击“开始测试” +6. `test-prompt.js` 从 `document.cookie` 读取不到 `ephron_csrf` +7. 前端仍继续向 `/api/prompts/{key}/test` 发送请求,`X-CSRF-Token` 为空 +8. 后端发现 `csrf_header` 与 `csrf_cookie` 缺失或不一致 +9. 返回 `403 CSRF token 验证失败` +10. 前端再将其表现为“安全令牌未初始化”类错误提示 + +### 5.3 为什么访问 `/admin/settings` 后会“恢复” + +因为后台设置页会调用: + +- `generate_csrf_token()` +- `set_csrf_cookie(...)` + +于是浏览器终于具备了 `ephron_csrf`。之后再回到详情页,同一份前端 JS 就能成功从 `document.cookie` 中读到 token,于是测试恢复正常。 + +这进一步证明: + +- 问题不是模型请求本身 +- 问题不是账号鉴权本身 +- 问题是 **公开详情页缺失 CSRF 初始化责任** + +--- + +## 六、修复目标 + +1. 已登录用户首次直接访问公开提示词详情页时,可直接使用测试功能 +2. 测试功能不再依赖用户事先访问后台页面进行“预热” +3. 保持现有 CSRF 防护机制,不通过移除校验来规避问题 +4. 前端在 token 缺失时给出清晰、可理解的错误提示 +5. 为该问题补齐自动化回归测试,避免未来再次回归 + +--- + +## 七、修复方案 + +## 7.1 方案 A:公开详情页 GET 路由补发 CSRF cookie(主修复,必须做) + +文件:`prompt/src/routes/pages.py` + +在渲染 `public/detail.html` 的公开详情页路由中,补齐与后台页面一致的初始化逻辑: + +1. 调用 `generate_csrf_token()` 生成 token +2. 使用 `TemplateResponse` 先构造响应对象 +3. 调用 `set_csrf_cookie(response, csrf_token, is_development=IS_DEVELOPMENT)` +4. 返回该 response + +### 设计要求 + +- 实现方式应尽量复用后台页面已有模式 +- 不新增新的 CSRF 机制分支 +- 不改动接口鉴权模型 +- 不改动测试 API 的 CSRF 校验规则 + +### 预期效果 + +- 用户打开详情页即自动获得 `ephron_csrf` +- 点击测试时,前端能直接从 cookie 取到有效 token +- 后端的现有校验链路无需修改即可通过 + +--- + +## 7.2 方案 B:前端增加 token 缺失的显式防御(兜底增强,建议一并做) + +文件:`prompt/static/js/test-prompt.js` + +在发起 `fetch()` 前增加前置判断: + +```javascript +const csrfToken = document.cookie.match(/ephron_csrf=([^;]+)/)?.[1] || ''; +if (!csrfToken) { + throw new Error('安全令牌未初始化,请刷新页面后重试'); +} +``` + +### 目的 + +- 避免把空 token 静默发给后端 +- 让用户看到更明确的错误原因 +- 即使未来某次页面初始化异常,也能更快暴露问题归因 + +### 说明 + +这不是根修复,只是防御性增强。真正的根因修复仍然是方案 A。 + +--- + +## 7.3 本次不建议采用的替代方案 + +### 替代方案:新增单独的 CSRF 初始化 API + +例如新增: + +- `GET /api/auth/csrf` + +并在前端缺 token 时先请求该接口。 + +### 不作为首选原因 + +- 当前项目已有稳定的服务端渲染页面 + `set_csrf_cookie` 模式 +- 新增 API 会引入额外异步初始化链路和更多状态分支 +- 对当前问题来说属于过度设计 + +除非后续要支持更多纯前端异步挂载场景,否则现阶段没有必要。 + +--- + +## 八、实现清单 + +### 8.1 后端改动 + +文件:`prompt/src/routes/pages.py` + +#### 需要做的事 + +- 导入: + - `generate_csrf_token` + - `set_csrf_cookie` +- 在 `prompt_detail()` 中: + - 生成 `csrf_token` + - 将原本直接返回的 `TemplateResponse` 改为先赋值给 `response` + - 调用 `set_csrf_cookie(response, csrf_token, is_development=IS_DEVELOPMENT)` + - 返回 `response` + +#### 约束 + +- 不改动详情页已有模板参数结构,避免影响正文渲染 +- 不需要将 token 注入模板 DOM;cookie 已足够供当前 JS 使用 + +### 8.2 前端改动 + +文件:`prompt/static/js/test-prompt.js` + +#### 需要做的事 + +- 在 `runTest()` 中的 `fetch()` 前增加 token 非空校验 +- 优化错误提示映射: + - `401` → 需要登录后才能测试 + - `403` 且 detail 含 CSRF → 安全令牌失效或未初始化,请刷新页面后重试 + - 其他错误 → 保持现有通用错误兜底 + +#### 约束 + +- 不改变现有 SSE 流式处理逻辑 +- 不改变现有模型选择、变量注入、Markdown 渲染逻辑 + +--- + +## 九、测试方案 + +## 9.1 自动化测试补齐 + +### 测试 1:公开详情页 GET 会设置 `ephron_csrf` + +建议位置: + +- `prompt/tests/test_prompt_service_api.py` +- 或更合适的 pages 路由测试文件 + +#### 步骤 + +1. 创建一个 active prompt +2. 构造已登录 client +3. `GET /prompts/{key}` +4. 断言 response / client cookies 中存在 `ephron_csrf` + +#### 验证点 + +- 详情页自身承担了 CSRF 初始化职责 + +### 测试 2:首次访问详情页后可直接调用测试接口 + +#### 步骤 + +1. 构造已登录 client +2. `GET /prompts/{key}` +3. 从 client cookies 读取 `ephron_csrf` +4. 直接 `POST /api/prompts/{key}/test` +5. 请求头中传相同 token +6. 断言不返回 `403 CSRF token 验证失败` + +#### 验证点 + +- 详情页访问一次后,测试能力可以独立工作 + +### 测试 3:未登录用户仍被正确拦截 + +#### 步骤 + +1. 不注入 `ephron_auth` +2. 请求 `/api/prompts/{key}/test` +3. 断言返回 `401 需要登录` + +#### 验证点 + +- 修复不应削弱登录鉴权 + +### 测试 4:缺失 token 时仍正确返回 CSRF 错误 + +#### 步骤 + +1. 构造已登录 client +2. 不提供 `ephron_csrf` 或提供空 header +3. 调用测试接口 +4. 断言返回 `403` + +#### 验证点 + +- 修复不应通过绕过 CSRF 校验来“修好”功能 + +--- + +## 9.2 手工验收 + +### 场景 A:首次直接测试 + +1. 使用已登录账号登录 +2. 直接访问任意公开提示词详情页 +3. 不访问任何 `/admin/*` 页面 +4. 切到“测试”标签 +5. 输入内容后点击“开始测试” + +**预期:** + +- 请求成功 +- 页面正常流式输出结果 +- 不再出现“安全令牌未初始化”或 `CSRF token 验证失败` + +### 场景 B:未登录测试 + +1. 清除登录态 +2. 打开公开详情页 +3. 执行测试 + +**预期:** + +- 页面提示需要登录 +- 后端返回 401 + +### 场景 C:异常 token 场景 + +1. 手动清除或伪造 `ephron_csrf` +2. 保留登录态 +3. 再次点击测试 + +**预期:** + +- 页面出现清晰的 token 失效/未初始化提示 +- 不应只显示模糊的“请求失败” + +--- + +## 十、验收标准 + +满足以下条件才算本 PRD 完成: + +1. 公开提示词详情页首次加载时即可写入 `ephron_csrf` +2. 已登录用户无需访问后台页面即可直接使用测试功能 +3. `/api/prompts/{key}/test` 仍保留现有 CSRF 校验机制 +4. 未登录用户仍返回 401,不放宽权限 +5. 前端在 token 缺失时给出明确错误提示 +6. 自动化测试覆盖“详情页初始化 token”的集成场景 +7. 不引入对正文展示、示例展示、模型下拉和测试流式输出的回归问题 + +--- + +## 十一、风险与注意事项 + +### 11.1 不要用“删除 CSRF 校验”来修问题 + +这是最需要避免的误修方向。问题的本质是: + +- token 没初始化 + +而不是: + +- token 校验太严格 + +### 11.2 注意 cookie 域与 secure 配置 + +`set_csrf_cookie()` 的行为受共享配置影响。上线时需确认: + +- `CSRF_COOKIE_DOMAIN` +- `secure` +- `samesite` + +与 `prompt.ephron.ren` 当前部署环境兼容。 + +### 11.3 不要让公开详情页继续隐式依赖后台页 + +本次修复的核心目标之一,就是消除: + +- “访问过 `/admin/settings` 才能测试成功” + +这种不合理依赖关系。 + +--- + +## 十二、结论 + +本问题的本质不是账号错误,也不是模型配置错误,而是: + +> **公开提示词详情页提供了测试能力,却没有初始化该能力所需的 CSRF cookie。** + +因此用户即便已经登录,测试功能仍然会失败。正确修复方式应是: + +- 由公开详情页 GET 路由主动完成 CSRF 初始化 +- 前端增加 token 缺失的显式兜底提示 +- 保持后端现有安全校验不变 + +这是一次典型的“能力挂载页未承担其初始化责任”的问题,修复后应确保详情页测试能力真正独立、自洽、可首次即用。