Record real Playwright ledgers, mark 20 audits and 9 remediations done, and leave management-implementation-plan as plan-only.
3.7 KiB
3.7 KiB
title, status, created, updated, severity
| title | status | created | updated | severity |
|---|---|---|---|---|
| 文档职责伪造与保存时重取 ETag 可覆盖并发正文 | done | 2026-09-13 | 2026-09-22 | critical |
文档职责伪造与保存时重取 ETag 可覆盖并发正文
W3 UI-rerun 关闭(2026-09-22):
status=done。真实 Playwright 账本 PASS:ui-acceptance-results.json(main UI-00..16)+ui-acceptance-results-post-restart.json(UI-15)。任务 tip 已合入本地dev;详见 short-term 与 测试报告。
现象
静态审查与 E2E UI-13 路径暴露两类产品缺陷:
- 职责提升:审核人 B 或项目外陌生人打开受控文档后,工作台把其当成编制人,出现正文编辑 / 送审动作。
- 并发覆盖:两窗口同开同一修订时,后保存方在点击保存瞬间重新 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 缺陷 |
独立审查补强(同一分支):
| 层 | 问题 | 风险 |
|---|---|---|
| PATCH | resolvePatchEtag 在 controlled 缺基线时仍返回 0:0 |
服务异常被当成合法 bootstrap,可能覆盖已有账本 |
| hydrate | revision-state 5xx/网络失败只写 undefined 仍返回可编辑 AST | 用户能改正文,保存再 PATCH 0:0 |
| 批注 | viewer 被 cast 为 reviewer/approver,canAnnotate 对 viewer 开放 |
查阅者可新增评审批注 |
| 职责 | isLegacyLocalDoc = !ids:服务端 duty id 为 0 仍走 name fallback |
同名用户被提权为编制人 |
安全含义:职责伪造是越权写(viewer/reviewer 可改受控正文并送审)。ETag 刷新或 0:0 误用是完整性破坏。viewer 批注是越权写元数据。
严重度
critical
关联整改
- Remediation: ../remediation/2026-09-13-document-revision-duty-etag-overwrite.md
- Short-term: ../ops/short-term/2026-09-13-document-revision-e2e-reject-routing.md
- 分支:
fix/document-revision-e2e-reject-routing
证据
- 代码(修复前):
internal/docgov/project_docs.gosummary 无职责列;web/src/lib/deliverableSync.tsauthor: fallbackUser.name;web/src/App.tsx默认'author'与保存前getRevisionState - 单测钉住:B/陌生人不成 author;stale hydrate ETag 用于 PATCH
- 补强单测:controlled 缺 ETag/5xx 不可保存或不可打开;freeform 可
0:0;viewer 不能批注;服务端 duty id=0 不按 name 提权 - 本轮不跑真实 E2E、不提交 evidence assets
开放问题
- 占槽后真实 UI-13a/13b 重跑;CURRENT 仍
in_progress,不得标done