docs: record duty forgery and ETag overwrite risks

Document the privilege-escalation and silent-overwrite hazards; keep
the reject-routing task in_progress pending real UI rerun.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-13 17:02:23 +00:00
co-authored by Cursor
parent be33409297
commit 0a34daf4b5
4 changed files with 108 additions and 8 deletions
@@ -0,0 +1,48 @@
---
title: "文档职责伪造与保存时重取 ETag 可覆盖并发正文"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
severity: critical
---
# 文档职责伪造与保存时重取 ETag 可覆盖并发正文
## 现象
静态审查与 E2E UI-13 路径暴露两类产品缺陷:
1. **职责提升**:审核人 B 或项目外陌生人打开受控文档后,工作台把其当成编制人,出现正文编辑 / 送审动作。
2. **并发覆盖**:两窗口同开同一修订时,后保存方在点击保存瞬间重新 GET 最新 ETag,If-Match 对准已更新账本,409 不会出现,先保存方的正文被静默覆盖。
## 根因分析
| 层 | 问题 | 风险 |
|---|---|---|
| API | `ListWorkflowDocumentsByProject` / `GET projects/{id}/documents` 的 summary 不投影 `document_records` 的 author/reviewer/approver id+name | 前端无权威职责,只能猜 |
| 前端 merge | `mergeWorkflowDocumentsFromServer` 用当前登录用户名伪造 `author`,审核/批准写死「李主管」「王总监」 | B 登录后 `author===自己`,权限矩阵显示编制人 |
| 角色解析 | `currentDocRole` 比 name,失败默认 `'author'` | 未知身份 fail-open,陌生人获得编辑/送审 |
| 保存 | `handleSaveBlockEdit` 在 PATCH 前 `getRevisionState` 取最新 ETag | 打开时基线被丢弃;后写覆盖先写 |
| E2E | UI-13b 在 A1 完整保存后才打开 A2 编辑器 | 不是真实竞争,无法钉住上述 ETag 缺陷 |
安全含义:职责伪造是**越权写**(viewer/reviewer 可改受控正文并送审)。ETag 刷新是**完整性破坏**(并发编辑丢失且无冲突提示)。
## 严重度
`critical`
## 关联整改
- Remediation: [../remediation/2026-09-13-document-revision-duty-etag-overwrite.md](../remediation/2026-09-13-document-revision-duty-etag-overwrite.md)
- Short-term: [../ops/short-term/2026-09-13-document-revision-e2e-reject-routing.md](../ops/short-term/2026-09-13-document-revision-e2e-reject-routing.md)
- 分支: `fix/document-revision-e2e-reject-routing`
## 证据
- 代码(修复前):`internal/docgov/project_docs.go` summary 无职责列;`web/src/lib/deliverableSync.ts` `author: fallbackUser.name`;`web/src/App.tsx` 默认 `'author'` 与保存前 `getRevisionState`
- 单测钉住:B/陌生人不成 author;stale hydrate ETag 用于 PATCH
- 本轮不跑真实 E2E、不提交 evidence assets
## 开放问题
- 占槽后真实 UI-13a/13b 重跑;CURRENT 仍 `in_progress`,不得标 `done`
+2 -1
View File
@@ -5,7 +5,7 @@
## 焦点
1. **DOCREV-RT**: [E2E UI-05b 钉住 intake 并收紧历史比较/冲突](short-term/2026-09-13-document-revision-e2e-reject-routing.md)(`fix/document-revision-e2e-reject-routing`:不改产品逻辑;不重跑 E2E / 不操作容器)
1. **DOCREV-RT**: [E2E UI-05b + 职责/ETag 产品修复](short-term/2026-09-13-document-revision-e2e-reject-routing.md)(`fix/document-revision-e2e-reject-routing`:权威职责 + 打开时基线 ETag;不重跑 E2E / 不操作容器;仍 `in_progress`)
2. **CHAT**: [刷新恢复整改已实施](short-term/2026-09-13-chat-history-refresh-remediation.md)(待合 dev + PI E2E 补录)
3. **CI**: `TEST_DATABASE_URL` + 真实 PI E2E 实录;INTENT 发布 blocker 见 conversation-intent short-term
@@ -60,3 +60,4 @@
| 行动表状态假阳性审计 | [../audit/2026-09-13-document-revision-quality-status-false-positive.md](../audit/2026-09-13-document-revision-quality-status-false-positive.md) |
| 待确认报告选择失败审计 | [../audit/2026-09-13-document-revision-pending-report-selection.md](../audit/2026-09-13-document-revision-pending-report-selection.md) |
| UI-05b 拒绝路由失败审计 | [../audit/2026-09-13-document-revision-e2e-reject-routing.md](../audit/2026-09-13-document-revision-e2e-reject-routing.md) |
| 职责伪造与 ETag 覆盖审计 | [../audit/2026-09-13-document-revision-duty-etag-overwrite.md](../audit/2026-09-13-document-revision-duty-etag-overwrite.md) |
@@ -13,16 +13,17 @@ mutex: document-revision-runtime-ui
## 目标
第五轮 UI-05b 首错修复:拒绝用例 prompt 必须带「当前受控文档《用户初始需求收集表》」,并在本轮 `POST /tasks/runs` 上严格断言 `task_key==='intake'` 且 `doc_id===controlledDocId`。同步收紧 UI-10/12/13/14 真实用户路径。不改产品业务代码。
第五轮 UI-05b 首错修复:拒绝用例 prompt 必须带「当前受控文档《用户初始需求收集表》」,并在本轮 `POST /tasks/runs` 上严格断言 `task_key==='intake'` 且 `doc_id===controlledDocId`。同步收紧 UI-10/12/13/14 真实用户路径。本轮追加产品修复:权威文档职责 + 打开时基线 ETag。
## 范围
- `web/e2e/document-revision-runtime.spec.ts`
- 文档:audit / CURRENT(mutex `document-revision-runtime-ui`)
- `internal/docgov/project_docs.go`(summary 职责投影)
- `web/src` 职责字段、`resolveCurrentDocRole`、hydrate/PATCH ETag
- `web/e2e/document-revision-runtime.spec.ts`(UI-13b 真实竞争)
- 文档:audit / remediation / CURRENT(mutex `document-revision-runtime-ui`)
## 非目标
- 不改 `templateMatch` / Toolbar / decision / PATCH
- 不降低已有业务断言或超时;不伪造、不直接打 API
- 不触碰/提交 `.doc/test-reports/assets`;不操作容器;本轮不重跑 E2E
- 不改 FEATURE-MATRIX
@@ -33,7 +34,9 @@ mutex: document-revision-runtime-ui
2. [x] UI-05b prompt + tasks/runs 契约断言 + 真实打开拒绝
3. [x] UI-10 历史 dialog 内选 A/B 比较并关闭;UI-12 项目不可见;UI-13 拆成审核人只读 + 双 A 冲突;UI-14 同时锁 v/r
4. [x] 仅修 05a/05b/10 遗留弹窗对 UI-06 的确切遮挡
5. [ ] 占槽后真实 UI 重跑(本轮不做)
5. [x] 产品修复:权威职责 + viewer fail-closed + hydrate 基线 ETag
6. [x] UI-13b:A1 保存前 A2 编辑器已打开并填入文本;两窗口同一 r/etag
7. [ ] 占槽后真实 UI 重跑(本轮不做)
## 验收标准
@@ -41,11 +44,15 @@ mutex: document-revision-runtime-ui
- [x] UI-10 不点全局工具条「比较」
- [x] UI-12 不接受「项目可见但只查 marker」
- [x] UI-13a/13b 结果分别记录;B 不作正文编辑者
- [x] B/陌生人不会变 author;A 仍 author(Go + 前端单测)
- [x] stale hydrate ETag 用于 PATCH(不能用最新 ETag 掩盖冲突)
- [x] web tests + `tsc`/build + `git diff --check`
- [ ] 真实 UI-05b+ 后段 PASS 后才可将本任务标 `done`
- [ ] 真实 UI-05b+ 后段及 UI-13a/13b PASS 后才可将本任务标 `done`
## 相关链接
- Audit: [../../audit/2026-09-13-document-revision-e2e-reject-routing.md](../../audit/2026-09-13-document-revision-e2e-reject-routing.md)
- Audit (05b 路由): [../../audit/2026-09-13-document-revision-e2e-reject-routing.md](../../audit/2026-09-13-document-revision-e2e-reject-routing.md)
- Audit (职责/ETag): [../../audit/2026-09-13-document-revision-duty-etag-overwrite.md](../../audit/2026-09-13-document-revision-duty-etag-overwrite.md)
- Remediation: [../../remediation/2026-09-13-document-revision-duty-etag-overwrite.md](../../remediation/2026-09-13-document-revision-duty-etag-overwrite.md)
- Evidence: [../../test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui05b-reject-routing-failure](../../test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui05b-reject-routing-failure)
- 前序: [2026-09-13-document-revision-e2e-pending-flow.md](./2026-09-13-document-revision-e2e-pending-flow.md)
@@ -0,0 +1,44 @@
---
title: "权威文档职责 + 打开时基线 ETag"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
---
# 权威文档职责 + 打开时基线 ETag
## 背景
链到 [../audit/2026-09-13-document-revision-duty-etag-overwrite.md](../audit/2026-09-13-document-revision-duty-etag-overwrite.md)。
陌生人/审核人被抬成编制人可改受控正文;保存时重取最新 ETag 会掩盖 409 并覆盖并发编辑。
## 目标
1. 列表 summary 返回台账 author/reviewer/approver 的 id+name;前端保留权威字段,禁止用当前登录用户伪造 author。
2. `currentDocRole` 按权威 user id 解析;name 仅兼容旧本地文档;未知身份 fail-closed 为 `viewer`;viewer 无编辑/审核/批准动作。
3. 受控正文 hydrate 同时取 revision-state ETag 写入 AST/缓存;人工 PATCH 只用该基线;成功后用响应 `state.etag` 更新;冲突保留编辑器与文本。无账本 bootstrap/freeform 仍可用 `0:0`。
4. UI-13b:A1 保存前两窗口已加载相同 r/etag,且 A2 编辑器已打开并填入文本。
## 分波次计划
| 波次 | 内容 | 验收 |
|------|------|------|
| Wave 1 | Go summary 投影职责 + 单测 | B 不是 author;A 仍是 author |
| Wave 2 | 前端 merge / `UserRole=viewer` / `resolveCurrentDocRole` | B/陌生人不成 author;viewer 无动作 |
| Wave 3 | hydrate 基线 ETag + PATCH 不再 GET | stale ETag 单测;不能用最新 ETag 掩盖冲突 |
| Wave 4 | UI-13b 真实竞争脚本 | A2 先开编辑器再 A1/A2 保存 |
| Wave 5 | 占槽真实 UI(本轮不做) | UI-13a/13b 实战 PASS 后才可 done |
## 验收标准
- [x] Go `ListWorkflowDocumentsIncludesAuthoritativeDuties`
- [x] 前端 merge + `resolveCurrentDocRole` 单测
- [x] stale hydrate ETag 用于保存(纯函数 + hydrate 单测)
- [x] UI-13b 脚本不降级(本轮不跑真实 E2E)
- [ ] 真实 UI 重跑 PASS
## 相关链接
- Audit: [../audit/2026-09-13-document-revision-duty-etag-overwrite.md](../audit/2026-09-13-document-revision-duty-etag-overwrite.md)
- Short-term: [../ops/short-term/2026-09-13-document-revision-e2e-reject-routing.md](../ops/short-term/2026-09-13-document-revision-e2e-reject-routing.md)