merge: candidate integrate-review-fixes (report + P0/P1 fixes) into dev
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
---
|
||||
title: "系统全面 Code Review(local_dev tip)"
|
||||
status: done
|
||||
created: 2026-09-26
|
||||
owner: Cursor
|
||||
task_id: "system-code-review-2026-09-26"
|
||||
policy_revision: "agent-governance-2026-09-21-r1 + cursor-governance-2026-09-24-r1 @ f4c1b73f28580ad3df6db4f9ec0a80057006e516"
|
||||
review_state: accepted
|
||||
integration_state: integrated
|
||||
deployment: not_required
|
||||
cleanup: done
|
||||
remote_sync: not_requested
|
||||
updated: 2026-09-26
|
||||
severity: high
|
||||
---
|
||||
|
||||
# 系统全面 Code Review(local_dev tip)
|
||||
|
||||
## 范围与授权
|
||||
|
||||
- **用户目标**:对系统做一次全面 code review,并按规范形成报告;**不允许实施修复**,只形成报告。
|
||||
- **被审对象**:本地 `dev` tip `f4c1b73f28580ad3df6db4f9ec0a80057006e516`(merge: swboot-closeout-2026-09-26-r3);运行态参考 no-docker local_dev(`bin/agent-server`,schema 75),本报告以代码与门禁证据为主。
|
||||
- **允许写路径**:`.doc/`(本审计主文档、INDEX、short-term 任务记录)。
|
||||
- **交付终点**:可审审计报告 + 索引挂接;不改业务代码、不改 gap-matrix / FEATURE-MATRIX 状态值、不部署。
|
||||
- **非目标**:逐文件全量行审、依赖 CVE 扫描、前端 XSS 系统化、动态渗透、实施整改。
|
||||
- **方法**:并行四轴审查(架构边界 / 安全 / 可维护性 / 关键执行路径)+ 本机 `./scripts/arch-check.sh --mode block`、规模盘点与关键片段复核。
|
||||
|
||||
## 问题登记与经验检索
|
||||
|
||||
- **issue_id**:本轮不新建 ISSUES 行;发现项以本报告 FIND-* 编号跟踪。若后续授权修复,再按 [闭环规范](../ops/2026-09-23-issue-learning-workflow.md) 升 issue。
|
||||
- **检索词**:`IDOR`、`GetUserContext`、`session_id`、`validated_only`、`fencing`、`arch-check`、`table_owners`、`ALLOW_IN_MEMORY`、`sh -c`、`swboot`。
|
||||
- **命中**:历史审计见 [INDEX](./INDEX.md)(模块耦合 2026-09-20、对话办公盘点 2026-09-25、gap-matrix);经验库无与本轮 FIND-SEC-01 完全同题的已关闭 lesson。**采纳检查**:对照模块开发规范 §2/§4、authz fail-closed 惯例、intentflow 注释契约。
|
||||
|
||||
## 总判(Verdict)
|
||||
|
||||
`arch-check --mode block` **PASS**,且多项生产门禁(任务路径 stub 拒绝、FAV outbox consumer 多层禁用、docgov 角色不信任 body)仍然有效。但系统存在三类结构性风险:
|
||||
|
||||
1. **安全旁路**:会话上下文 IDOR(已证实);Authorizer 无池 fail-open;swboot `sh -c` + 前缀白名单。
|
||||
2. **门禁空心化**:未登记表的 SQL 写被 arch-check **静默跳过**;server 仍持 `Runtime.Pool` 并直查/直写。
|
||||
3. **多入口编排**:docgov / 多意图 / flag 面平行三套,关闭开关时存在静默降级。
|
||||
|
||||
本轮 **不改变** gap-matrix 的 `production_ready` 叙事;下列 critical/high 建议作为独立 remediation 候选,待授权后再立项。
|
||||
|
||||
## 现象
|
||||
|
||||
只读审查与门禁核验,无用户报障个案。审查期望:合入 swboot / dialogue-ctx / jevdecide / semantic-replace 后的 tip 在模块边界、鉴权与关键执行路径上仍满足生效规范。实际:门禁绿,但多处规范语义与实现分叉(见 Findings)。
|
||||
|
||||
## Findings(按严重度)
|
||||
|
||||
置信度说明:`verified` = 父会话已打开源码复核;`agent` = 子代理报告、抽样一致;`inferred` = 机制推断未跑动态用例。
|
||||
|
||||
### Critical
|
||||
|
||||
#### FIND-SEC-01 — 会话消息 IDOR(`session_id` 无归属校验)
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 类别 | 授权 / IDOR |
|
||||
| 位置 | `internal/server/handlers_context.go:17-50`;`internal/ctxmgr/manager.go:377-385` |
|
||||
| 证据 | `handleGetUserContext` 仅校验 `user_id` 为本用户/管理员;`GetUserContext` 对 `chat_messages` 使用 `WHERE session_id = $1` **无 owner 条件**。对照 `chatflow` 读路径会校验会话归属。 |
|
||||
| 影响 | 已登录用户若获知他人 `session_id`,可读取对方最近聊天正文。 |
|
||||
| 置信度 | verified · high |
|
||||
| 修复方向(不实施) | 复用 chatflow 会话授权;或按 `session.owner_id`/`user_id` 过滤后再返回。 |
|
||||
|
||||
#### FIND-ARCH-01 — arch-check 对未登记表静默跳过(表唯一写者门禁盲区)
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 类别 | 门禁 / 模块边界 |
|
||||
| 位置 | `internal/archtest/scan.go:198-205`;`module-boundaries.yaml` `table_owners`(约 59 表) |
|
||||
| 证据 | `if !tracked { continue }`;本机 tip:`swboot_controlled_executions` / `projects` / `project_source_bindings` / `outbox_events` **UNTRACKED**;`arch-check --mode block` 仍 PASS。 |
|
||||
| 影响 | 新表(含 swboot)可任意包写入而不触发 block;「表唯一写者」对未入库表无强制力。 |
|
||||
| 置信度 | verified · high |
|
||||
| 修复方向 | 未登记表的生产写应变 fail-closed,或强制登记。 |
|
||||
|
||||
#### FIND-ARCH-02 — server 持 Pool 直装 swboot 并 SQL 旁路
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 类别 | 组合根越界 |
|
||||
| 位置 | `internal/server/swboot_handlers.go`(NewPGRepoAllowlist / ControlledPatchExecutor / 直查 bindings & executions) |
|
||||
| 证据 | 与模块规范 §2.1「server 只拿用例或端口」冲突;`app.Build` 已另有 allowlist 装配。 |
|
||||
| 影响 | HTTP 层成为第二组合根;边界回归。 |
|
||||
| 置信度 | agent · high(与规范条文一致) |
|
||||
| 修复方向 | 收成 app 用例端口;禁止 handler 使用 `rt.Pool`。 |
|
||||
|
||||
### High
|
||||
|
||||
#### FIND-SEC-02 — Authorizer 无池时成员判定 fail-open
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 类别 | 鉴权 fail-open |
|
||||
| 位置 | `internal/authz/authorizer.go:112-114`、`205-207` |
|
||||
| 证据 | `!a.enabled()`(pool==nil)时 `IsProjectMember`/`IsProjectOwner` 对 `userID>0` 返回 true;组合根始终 `NewAuthorizer(pool)`,nil pool 时 `authorizer()!=nil`,绕过「nil → failClosed」分支。 |
|
||||
| 影响 | AllowInMemory / 无库误配时,任意登录用户可访任意 `project_id`。生产有 `ALLOW_IN_MEMORY` 门禁则触发面缩小,**机制本身仍 fail-open**。 |
|
||||
| 置信度 | verified · high |
|
||||
|
||||
#### FIND-SEC-03 — swboot 受控执行:`sh -c` + basename 前缀白名单
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 类别 | 命令注入面 / 边界语义 |
|
||||
| 位置 | `internal/swboot/executor.go`(`sh -c`、前缀匹配);`ApproveMerge` 恒拒 |
|
||||
| 证据 | 允许判定为字段首段 basename 前缀;命令经 shell 执行。执行器自身不做 merge/push,但若 admin allowlist 含 `git`/`bash` 等,客户端可间接 push/merge/deploy。 |
|
||||
| 影响 | 在 creator + 宽松 allowlist 前提下,隔离 worktree 内可越出「测试命令」意图。 |
|
||||
| 置信度 | agent · high |
|
||||
| 说明 | 与 MATRIX「永不 merge/push/deploy」在「执行器 API」语义下仍成立;真正边界在 **admin 命令白名单粒度**。 |
|
||||
|
||||
#### FIND-FLOW-01 — intentflow `validated_only`/`proposed` 可经 confirm 进入 dispatch
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 类别 | 执行模式门禁 |
|
||||
| 位置 | `internal/app/intentflow/plans.go:365-376`;`intent_plan_handlers.go:86-88`;`ExecuteClaimed` 不读 `ExecutionMode` |
|
||||
| 证据 | `DecisionConfirm` **允许** `PlanProposed`;确认后 HTTP 调用 `DispatchEligible`;`AllowedModes` 策略字段几乎未被执行路径使用。 |
|
||||
| 影响 | shadow「安全」依赖「无人 Confirm」,而非执行器硬拒绝;U10/U11 live 已接线时可升级为真实写。 |
|
||||
| 置信度 | verified · high(静态);未做动态 confirm 用例 |
|
||||
|
||||
#### FIND-QUAL-01 — docgov / 多意图多入口并行(职责扩散)
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 类别 | 可维护性 / 双写路径 |
|
||||
| 位置 | chat `docgov.*` PendingAction;`app/docgov_owner.go` `document.*`;`server/handlers_docgov.go`;intent vs intentflow 双套 flags |
|
||||
| 证据 | 同一 `GovernanceEngine` 三入口;`intent.FeatureFlags` 与 `intentflow.FeatureFlags` 并存;Coordinator ~40 intent case + intentflow OwnerMux 平行。 |
|
||||
| 影响 | 鉴权/确认/审计易漂移;回归矩阵笛卡尔积;开关关闭时 Mux→Query 静默降级(FIND-FLOW-02)。 |
|
||||
| 置信度 | agent · high |
|
||||
|
||||
#### FIND-ARCH-03 — bizdev / Runtime / 万能 Engine 门面残留
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 类别 | 架构债 |
|
||||
| 位置 | `internal/bizdev/service.go`;`internal/app/app.go` Runtime;`docgov`/`coordinator` Engine |
|
||||
| 证据 | bizdev 自开池并装配多引擎;Runtime 暴露 Pool+数十服务;Engine 方法面极大。arch-check allowlist 放行上帝包,PASS ≠ 模块化健康。 |
|
||||
| 影响 | 新功能默认堆 handler/Runtime;与解耦批次目标持续摩擦。 |
|
||||
| 置信度 | agent · high |
|
||||
|
||||
### Medium
|
||||
|
||||
| ID | 摘要 | 关键位置 | 置信度 |
|
||||
|---|---|---|---|
|
||||
| FIND-SEC-04 | 授权拒绝 404/403 区分,泄露资源存在性 | `task_run_auth.go`、`auth_helpers.go` | agent |
|
||||
| FIND-SEC-05 | Chat SSE:阶段拒绝在 Flush 之后;`user_role` 可写入会话上下文 | `sse.go` | agent |
|
||||
| FIND-SEC-06 | 对话 `agent.NewBackend` 对未知名默认 stub;生产密钥校验不禁 stub | `agent/backend.go`、`config.go` | agent |
|
||||
| FIND-SEC-07 | 未认证 `/api/v1/vault/status` 暴露 FAV 开关 | `auth_middleware.go`、`vault.go` | agent |
|
||||
| FIND-SEC-08 | 会话 token 存 `localStorage`(XSS 放大) | `web/src/api/auth.ts` | agent |
|
||||
| FIND-DOC-01 | docgov 省略 fencing token 仍可写;heartbeat persist / squad reclaim fail-open | `docgov/lease.go` | agent |
|
||||
| FIND-DOC-02 | `CommitAgentVersion` 与 edit lease 解耦竞态面 | `docrevision/service_agent.go` | agent |
|
||||
| FIND-FLOW-02 | `OwnerMux` 在 U10/U11/U12 关闭时静默降级到 Query | `intentflow/mux.go` | agent |
|
||||
| FIND-ARCH-04 | notification handler 端口失败后回落 Pool SQL | `notification_handlers.go` | agent |
|
||||
| FIND-ARCH-05 | 提案批准后 handler 裸 goroutine 副作用 | `handlers_docgov.go` | agent |
|
||||
| FIND-ARCH-06 | 错误信封拼接 `err.Error()` | 多 handler | agent |
|
||||
| FIND-QUAL-02 | 全局 flag/metrics 损害并行测试隔离 | `intent/flags.go`、`intentflow/*` | agent |
|
||||
| FIND-QUAL-03 | 巨石文件逼近/超过规范阈值(见规模盘点) | squad/taskrun/server/web | verified |
|
||||
|
||||
### Low / 契约说明
|
||||
|
||||
| ID | 摘要 |
|
||||
|---|---|
|
||||
| FIND-JEV-01 | jevdecide assist 失败为 **fail-open 回规则**(不编造);若产品要求「jev 挂则拒答」则不满足——属契约澄清,非实现疏忽。 |
|
||||
| FIND-DOC-03 | summarizer 注释仍写「REPLACE 不接线」,与已合入 `applySemanticReplace` 矛盾。 |
|
||||
| FIND-EVT-01 | 事件 payload 密钥过滤面窄于 taskrun `StripSecrets`(子串级)。 |
|
||||
|
||||
## 同类问题与影响范围
|
||||
|
||||
| 关联场景 | 共用机制 | 检查与证据 | 判定 |
|
||||
|---|---|---|---|
|
||||
| chatflow 读会话 | 会话 owner 校验 | chatlog Adapter authorize | 主路径健全;**旁路** `GetUserContext` 缺校验 |
|
||||
| 任务 SSE 鉴权 | Flush 前 RequireProjectAccess | gap-matrix AUTH-TASK-SSE PASS | 项目门健全;阶段拒绝在流后属中危差异 |
|
||||
| FAV outbox | Enabled:false + Register/Claim 硬拒 | registry/dispatcher/eventsconsumer | **未见旁路打开** |
|
||||
| swboot ApproveMerge | 恒拒 | repo_allowlist | 自身 API 健全;命令白名单是真边界 |
|
||||
| 生产 ALLOW_IN_MEMORY | validateProductionSecrets | config.go | 非空 Env 的 prod 有防护;空 Env+内存组合仍危险 |
|
||||
|
||||
**未覆盖(明确边界)**:govulncheck/npm audit;MinerU/Embedding SSRF 动态验证;FAV degraded 跨项目读 E2E;Markdown XSS 系统化;全量 `BeginTx` 持锁外呼追踪。
|
||||
|
||||
## 方案与验收(仅建议,本轮不实施)
|
||||
|
||||
### 建议优先级
|
||||
|
||||
1. **P0**:FIND-SEC-01(IDOR)— 补会话归属;加回归(他用户 session_id → 403/空)。
|
||||
2. **P0**:FIND-ARCH-01 — arch-check 未登记表 fail-closed;补登记 swboot/projects 等属主。
|
||||
3. **P1**:FIND-SEC-02 — Authorizer `!enabled()` 改 fail-closed 或与 `failClosedAuthz` 对齐。
|
||||
4. **P1**:FIND-SEC-03 / FIND-FLOW-01 — argv 化命令 + 收紧白名单;Confirm 拒绝 `validated_only`/`proposed` 或执行器硬读 mode。
|
||||
5. **P2**:收敛 docgov/多意图单一写路径与单一 flag 面;收回 server Pool。
|
||||
|
||||
### 验收提示(未来修复任务)
|
||||
|
||||
按 [验证契约](../ops/2026-09-21-validation-contract.md)「问题修复的回归范围」:原例 + 同类旁路 + 正常授权路径;工程侧补 `archtest` 用例证明未登记表 block。
|
||||
|
||||
## 严重度
|
||||
|
||||
报告整体:`high`(存在已证实 IDOR + 门禁盲区;未做动态利用)。
|
||||
|
||||
## 关联整改
|
||||
|
||||
- 既有: [模块解耦分批](../remediation/2026-09-20-module-decoupling-batch-plan.md)、[对话办公扩展](../remediation/2026-09-25-dialogue-office-expansion-plan.md)、[gap-matrix](../remediation/gap-matrix.md)
|
||||
- **本轮未新建 remediation 文件**(用户禁止实施;方案仅建议)。授权后建议拆:`remediation/2026-09-26-system-code-review-followups.md`。
|
||||
|
||||
## 证据
|
||||
|
||||
### 门禁与规模(本机)
|
||||
|
||||
```text
|
||||
被审 SHA: f4c1b73f28580ad3df6db4f9ec0a80057006e516
|
||||
arch-check --mode block → production gate passed
|
||||
tracked table_owners ≈ 59;swboot_controlled_executions/projects/project_source_bindings/outbox_events = UNTRACKED
|
||||
|
||||
Go 生产文件 LOC 前列(非测试):
|
||||
cmd/bizdev-cli/main.go 1017
|
||||
internal/squad/workflow.go 963
|
||||
internal/server/task_run_handlers.go 858
|
||||
internal/taskrun/manager.go 813
|
||||
…
|
||||
|
||||
Web:
|
||||
web/src/App.tsx 1748
|
||||
web/src/engine/docxRenderer.ts 1263
|
||||
…
|
||||
```
|
||||
|
||||
### 子代理
|
||||
|
||||
| 轴 | 类型 | 用途 |
|
||||
|---|---|---|
|
||||
| 架构边界 | architecture-critic | 模块规范对抗审 |
|
||||
| 安全 | security-auditor | OWASP 向只读审(无 PoC) |
|
||||
| 可维护性 | thermo-nuclear-code-quality-review | 上帝对象/双路径/开关丛林 |
|
||||
| 关键路径 | code-explorer | intentflow/docgov/swboot/FAV/jev |
|
||||
|
||||
### 运行态(旁证,非本报告验收对象)
|
||||
|
||||
此前会话:no-docker `bin/agent-server` `vcs.revision` = tip;`/api/v1/health`+`/ready` ok,schema 75。
|
||||
|
||||
## 开放问题
|
||||
|
||||
1. FIND-FLOW-01:产品是否故意允许对 shadow `proposed` 计划一键 Confirm 升 live?若否,属缺陷;若是,应改文档/MATRIX 与执行器注释。
|
||||
2. FIND-JEV-01:assist「失败 fail-closed」的产品定义是否等于「回退规则」?
|
||||
3. 是否授权将 FIND-SEC-01/ARCH-01 升 ISSUES 并开 remediation?
|
||||
|
||||
## 修复经过与经验
|
||||
|
||||
- **本轮未实施任何代码修复**(用户硬约束)。
|
||||
- 不新增 lesson:发现尚未经验证修复闭环。
|
||||
- 复发提醒:旁路 HTTP(绕过 chatflow/Owner)是鉴权回归的高发点。
|
||||
|
||||
## 状态与交接
|
||||
|
||||
| 字段 | 值 |
|
||||
|---|---|
|
||||
| 任务状态 | `done`(研究产物可审) |
|
||||
| review_state | pending(待人工审阅报告) |
|
||||
| integration_state | pending(报告合入 local_dev 另走 integrate) |
|
||||
| deployment | not_required |
|
||||
| cleanup | pending |
|
||||
| remote_sync | not_requested |
|
||||
| 剩余工作 | 授权后开 P0 修复任务;本 worktree 合入/清理由独立维护者执行 |
|
||||
| 禁止续作 | 在本任务下直接改业务代码冒充「顺便修」 |
|
||||
|
||||
## Positive observations
|
||||
|
||||
1. `arch-check` 对**已登记** import/sql/path/event 仍 enforced。
|
||||
2. FAV outbox consumer `Enabled:false` 多层硬拒,未见旁路打开。
|
||||
3. 任务 SSE 项目鉴权在 Flush 前;docgov 角色取 principal 不信任 body。
|
||||
4. 语义摘要 shadow:lease 提交后再 Summarize,符合持锁不做外部 IO。
|
||||
5. swboot/jevdecide 领域包 import allowlist 自身较窄;污染主要在接线层。
|
||||
@@ -13,6 +13,14 @@
|
||||
|
||||
## 核心文档(按阅读顺序推荐)
|
||||
|
||||
### 系统全面 Code Review(2026-09-26)
|
||||
|
||||
**文件**: [2026-09-26-system-code-review.md](./2026-09-26-system-code-review.md)
|
||||
|
||||
**结论**: tip `f4c1b73f` 上 `arch-check --mode block` PASS,但存在已证实会话上下文 IDOR、arch-check 未登记表静默跳过、Authorizer 无池 fail-open、swboot `sh -c` 前缀白名单、intentflow `proposed` 可 Confirm 升 dispatch 等 critical/high 项;FAV outbox 禁用路径未见旁路。**本轮不实施修复**。
|
||||
|
||||
**任务**: [short-term](../ops/short-term/2026-09-26-system-code-review.md) · mutex `docs-audit-review`
|
||||
|
||||
### 对话办公能力全景盘点(2026-09-25)
|
||||
|
||||
**文件**: [2026-09-25-dialogue-office-capability-research.md](./2026-09-25-dialogue-office-capability-research.md)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
维护规则:[问题记录与开发经验闭环](../ops/2026-09-23-issue-learning-workflow.md)。每个问题一个稳定 ID;主记录是事实与状态来源,本表为可检索投影。新发现及时登记,修复后保留记录;发布 blocker 仍见 [gap-matrix](../remediation/gap-matrix.md)。
|
||||
|
||||
更新:2026-09-26。docgov 批准链:登记并修复 review_snapshot_id 装载缺失与 uow 行锁自死锁(resolved);memDocs 持久化失败缓存投毒(investigating)。历史 resolved 不表示本日产品回归通过。
|
||||
更新:2026-09-26。docgov 批准链:登记并修复 review_snapshot_id 装载缺失与 uow 行锁自死锁(resolved);memDocs 持久化失败缓存投毒(investigating)。历史 resolved 不表示本日产品回归通过。系统 code review P0/P1 批次修复落账(5 resolved + 1 deferred + 1 open,主记录见 [整改文档](2026-09-26-system-code-review-fixes.md),来源 [审计](2026-09-26-system-code-review.md))。
|
||||
|
||||
| issue_id | 模块 / 检索词 | 问题 | 严重度 | issue_state | owner | 证据日期 / 范围 | 唯一主记录 | 经验 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
@@ -21,3 +21,11 @@
|
||||
| ISSUE-20260926-docgov-snapshot-load | docgov / loadDocumentFromDBLocked / review_snapshot_id / 跨重启批准 | 文档装载 SELECT 漏 review_snapshot_id,重启/缓存未命中后最终批准必失败 | high | resolved | pi | 2026-09-26;隔离库 PG 回归 + dev 真实批准 200/state=approved(dev `121b33f8`) | [主记录](../test-reports/2026-09-26-rdev-p3-rerun-test-report.md#5-2) | 待补 LESSON |
|
||||
| ISSUE-20260926-docgov-uow-deadlock | docgov / persistApproveAndFAV / uow / FOR UPDATE / 57014 | docflow uow 行锁后 FAV 持久化自开第二连接更新同行,语句超时致死锁,所有最终批准失败 | critical | resolved | pi | 2026-09-26;生产形状 PG 回归(FOR UPDATE+FAV+Within)+ dev 真实批准转 200(dev `5d5cd5fd`) | [主记录](../test-reports/2026-09-26-rdev-p3-rerun-test-report.md#5-2) | 待补 LESSON |
|
||||
| ISSUE-20260926-docgov-memdocs-poison | docgov / memDocs / 状态机 / 持久化失败 / 缓存投毒 | 状态机先改内存后落库;持久化失败不复原,memDocs 缓存与 DB 永久分叉并掩盖真实错误(表现为「approved 状态」误报) | high | investigating | pi | 2026-09-26;dev 两次复现(快照/死锁两根因均被投毒掩盖);修复方向:失败时回滚内存快照或失效缓存条目 | [主记录](../test-reports/2026-09-26-rdev-p3-rerun-test-report.md#5-2) | 待补 LESSON |
|
||||
| ISSUE-20260926-ctx-idor | ctxmgr / GetUserContext / session_id / IDOR / handlers_context | GetUserContext 按 session_id 取消息无归属校验,登录用户可读他人会话正文(已证实) | critical | resolved | pi | 2026-09-26;handler 403 + SQL/内存双防线;内存 HTTP 与隔离 PG 回归 PASS | [主记录](../remediation/2026-09-26-system-code-review-fixes.md#1-find-sec-01-会话上下文-idorissue-20260926-ctx-idor) | [LESSON-20260926-failclosed-bypass-contract](../ops/lessons/2026-09-26-failclosed-bypass-contract.md) |
|
||||
| ISSUE-20260926-archcheck-untracked | archtest / table_owners / untracked / sql_write / 门禁盲区 | arch-check 对未登记表 SQL 写静默跳过,含 swboot_controlled_executions 等 ~50 表;门禁形同关闭 | critical | resolved | pi | 2026-09-26;未登记表 fail-closed + 补登 50 表 + 噪音豁免清单;arch-check block PASS | [主记录](../remediation/2026-09-26-system-code-review-fixes.md#2-find-arch-01-arch-check-未登记表静默跳过issue-20260926-archcheck-untracked) | [LESSON-20260926-failclosed-bypass-contract](../ops/lessons/2026-09-26-failclosed-bypass-contract.md) |
|
||||
| ISSUE-20260926-authz-failopen | authz / IsProjectMember / pool==nil / fail-open / NewInMemoryAuthorizer | Authorizer 无池时 IsProjectMember/IsProjectOwner 对任意登录用户返回 true,误配即全量放行 | high | resolved | pi | 2026-09-26;无池严格拒绝 + 显式内存授权接缝 + 组合根按 AllowInMemory 选择;隔离 PG authz 套件 PASS | [主记录](../remediation/2026-09-26-system-code-review-fixes.md#3-find-sec-02-authorizer-无池-fail-openissue-20260926-authz-failopen) | [LESSON-20260926-failclosed-bypass-contract](../ops/lessons/2026-09-26-failclosed-bypass-contract.md) |
|
||||
| ISSUE-20260926-swboot-shell-prefix | swboot / executor / sh -c / 命令注入 / 前缀白名单 / commandAllowed | 受控命令经 sh -c 执行且白名单前缀匹配(go 放行 go-evil);元字符可求值 | high | resolved | pi | 2026-09-26;argv 化 + basename 精确匹配;前缀伪装/不求值回归 PASS | [主记录](../remediation/2026-09-26-system-code-review-fixes.md#4-find-sec-03-swboot-sh--c--前缀白名单issue-20260926-swboot-shell-prefix) | [LESSON-20260926-failclosed-bypass-contract](../ops/lessons/2026-09-26-failclosed-bypass-contract.md) |
|
||||
| ISSUE-20260926-intentflow-validated-confirm | intentflow / DecisionConfirm / PlanProposed / validated_only / ExecuteClaimed | validated_only(shadow)计划可经 Confirm 进入 executing 并真实执行;执行器不读 ExecutionMode | high | resolved | pi | 2026-09-26;Confirm 拒 + 执行器硬拒双层 + 错误码/HTTP 映射;确认/派发双回归 PASS | [主记录](../remediation/2026-09-26-system-code-review-fixes.md#5-find-flow-01-validated_only-可-confirm-后-dispatchissue-20260926-intentflow-validated-confirm) | [LESSON-20260926-failclosed-bypass-contract](../ops/lessons/2026-09-26-failclosed-bypass-contract.md) |
|
||||
| ISSUE-20260926-dual-flags-drift | intent / intentflow / FeatureFlags / PlanTier / MULTI_INTENT_ENABLED / 静默漂移 | 双套 flags 可配置出「intentflow 升档但 intent 关闭」的矛盾组合并静默降级 | high | resolved | pi | 2026-09-26;组合根经 intentflow.ValidateFlagConsistency 启动期 fail-fast + 单测;入口收敛残差另立 deferred | [主记录](../remediation/2026-09-26-system-code-review-fixes.md#6-find-qual-01-双套-flags-静默漂移issue-20260926-dual-flags-drift含残差) | [LESSON-20260926-failclosed-bypass-contract](../ops/lessons/2026-09-26-failclosed-bypass-contract.md) |
|
||||
| ISSUE-20260926-governance-multi-entry | docgov / 多意图 / 多入口 / Coordinator / OwnerMux / GovernanceEngine | docgov 三入口 + Coordinator ~40 case 与 intentflow OwnerMux 平行,鉴权/确认/审计易漂移 | high | deferred | pi | 2026-09-26;flag 漂移面已修(dual-flags-drift);入口合并是多日级重构,需独立任务授权与回归矩阵 | [主记录](../remediation/2026-09-26-system-code-review-fixes.md#6-find-qual-01-双套-flags-静默漂移issue-20260926-dual-flags-drift含残差) | 沿用 failclosed-bypass-contract |
|
||||
| ISSUE-20260926-server-pool-wiring | server / swboot_handlers / Runtime.Pool / 组合根越界 / FIND-ARCH-02 | server 持 Pool 直装 swboot repo/executor 并直查表,成为第二组合根(本轮未授权修复) | critical | open | pi | 2026-09-26;代码审证据齐(审计 FIND-ARCH-02),待修复授权 | [主记录](../audit/2026-09-26-system-code-review.md) | 待修复后补 |
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
| owner | task | branch | worktree | mutex | status |
|
||||
|---|---|---|---|---|---|
|
||||
| pi | system-review-p0-p1-fixes-2026-09-26 | fix/system-review-p0-p1-fixes-2026-09-26 | .worktrees/system-review-p0-p1-fixes-2026-09-26 | review-fixes-batch | done |
|
||||
| Cursor | system-code-review-2026-09-26 | feature/system-code-review-2026-09-26 | .worktrees/system-code-review-2026-09-26 | docs-audit-review | done |
|
||||
| pi | swboot-closeout-2026-09-26-r3 | feature/swboot-closeout-2026-09-26 | .worktrees/swboot-closeout-2026-09-26 | workflow-design | done |
|
||||
| pi | swboot-closeout-2026-09-26-r2 | feature/swboot-closeout-2026-09-26 | .worktrees/swboot-closeout-2026-09-26 | workflow-design | in_progress |
|
||||
| pi | chat-semantic-replace-2026-09-26 | feature/chat-semantic-replace-2026-09-26 | .worktrees/chat-semantic-replace-2026-09-26 | chat | done |
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
lesson_id: LESSON-20260926-failclosed-bypass-contract
|
||||
lesson_state: validated
|
||||
created: 2026-09-26
|
||||
updated: 2026-09-26
|
||||
source_issues:
|
||||
- ISSUE-20260926-ctx-idor
|
||||
- ISSUE-20260926-archcheck-untracked
|
||||
- ISSUE-20260926-authz-failopen
|
||||
- ISSUE-20260926-swboot-shell-prefix
|
||||
- ISSUE-20260926-intentflow-validated-confirm
|
||||
reviewed_by: pi
|
||||
last_reviewed: 2026-09-26
|
||||
reviewed_revision: "fix/system-review-p0-p1-fixes-2026-09-26"
|
||||
keywords: [IDOR, 旁路, fail-open, fail-closed, 白名单前缀, sh -c, validated_only, table_owners, untracked, session_id, 归属校验]
|
||||
---
|
||||
|
||||
# 旁路入口与「未知即拒绝」契约
|
||||
|
||||
## 触发条件
|
||||
|
||||
- 新增/评审任何**绕过主用例路径**的 HTTP handler(尤其按 `session_id`/`project_id`/`user_id` 直接取资源的旁路端点)。
|
||||
- 写门禁扫描器、授权器或执行器时出现「未登记/未配置/依赖缺失」分支。
|
||||
- 注释或文档声称某 flag/mode 是安全边界(如 `validated_only must never be written as succeeded`)。
|
||||
- 命令/路径白名单实现(前缀匹配、`sh -c` 拼接)。
|
||||
|
||||
## 机制与应做检查
|
||||
|
||||
1. **主路径绿 ≠ 旁路绿**:主路径(chatflow/Owner 等)的鉴权不会自动覆盖旁路 handler。每个按标识取资源的入口都要复用同一归属校验(如 `GetSessionOwnerID`),并在 SQL/存储层再加归属条件作防御纵深——handler 层校验可能被未来调用方遗漏。
|
||||
2. **门禁对「未知」必须拒绝**:扫描器遇到未登记对象 `continue` 等于该维度门禁关闭(arch-check 未登记表曾静默跳过 ~50 张表的真实写)。正确形状:未登记→finding,假阳性用显式且可审计的豁免清单。
|
||||
3. **授权器缺依赖时不得放行**:`pool==nil` 返回 true 是 fail-open;需要演示/测试宽松语义时,用**显式命名的构造器**(`NewInMemoryAuthorizer`)+ 组合根按配置选择,默认路径严格拒绝。
|
||||
4. **契约写在注释里不算数**:`validated_only` 之类执行模式必须由执行器硬读硬拒(Confirm 拒 + ExecuteClaimed 拒双层),安全不能依赖「没人点 Confirm」或上游状态机不犯错。
|
||||
5. **白名单用精确匹配 + argv 执行**:basename 前缀匹配可被 `go-evil` 伪装;`sh -c` 让元字符求值。argv 化后元字符只是字面参数。
|
||||
|
||||
## 错误做法与原因
|
||||
|
||||
- 只给主路径补鉴权、把旁路留给「下次」——本仓 IDOR 事故高发位正是旁路 HTTP。
|
||||
- 用「未配置就放行」降低接线摩擦——组合根永远能构造成功,fail-open 在误配时静默生效。
|
||||
- 用测试 override `NewAuthorizer(nil)` 当「跳过检查」接缝——它同时是生产 fail-open 的机制本身;接缝必须显式命名。
|
||||
|
||||
## 验证方法
|
||||
|
||||
- 回归覆盖:原例(他户标识→403/空)、同类变体(不存在→同构拒绝、admin 越权面)、正常行为(本人→200 且数据完整)。
|
||||
- 门禁类:自测夹具证明未登记对象 block、已知假阳性不误报;全仓门禁在登记完成后 PASS。
|
||||
- 执行类:伪装命令拒绝、元字符不求值(marker 文件不产生)、owner 零调用计数。
|
||||
|
||||
## 适用边界
|
||||
|
||||
- 适用于进程内授权、静态扫描门禁、命令白名单、执行模式契约四类机制;不覆盖网络层/跨服务鉴权。
|
||||
- 豁免清单(如扫描噪音词)是启发式产物:新假阳性出现时应先确认确非真实表,再扩清单。
|
||||
|
||||
## 关联
|
||||
|
||||
- 来源修复:[2026-09-26 系统 code review P0/P1 修复](../../remediation/2026-09-26-system-code-review-fixes.md)
|
||||
- 发现审计:[2026-09-26 系统全面 code review](../../audit/2026-09-26-system-code-review.md)
|
||||
- 关联回归:`TestGetUserContext_*`、`TestUntrackedTableWriteIsBlocked`、`TestMembershipChecks_FailClosedWithoutPool`、`TestExecutor_CommandAllowlistIsExactNotPrefix`、`TestExecutor_NoShellEvaluation`、`TestConfirmRejectedForValidatedOnlyPlan`、`TestExecuteClaimedRefusesValidatedOnlyStep`
|
||||
@@ -10,5 +10,6 @@
|
||||
| LESSON-20260924-pi-tool-boundaries | PI、MCP、readonly、path、snapshot、process-group | 路径语义与快照边界一致,合法路径和拒绝路径都验收 | validated | [文件工具边界](2026-09-24-pi-tool-boundaries.md);2026-09-24 |
|
||||
| LESSON-20260924-cursor-hook-advisory-gates | Cursor、hooks、advisory、fail-open、probe、stdin | hook 与执行器分层;契约未知 fail-open;审计日志放 ignore 路径 | candidate | [客户端 hook 门禁](2026-09-24-cursor-hook-advisory-gates.md);2026-09-24 |
|
||||
| LESSON-20260924-deny-only-submit | 非编制、DOC_DUTY、400、run/edit、送审 | 权限哨兵映射 403;三道门协同放宽;ActorIsMember 不复用 RoleSystem | validated | [deny_only_submit](2026-09-24-deny-only-submit.md);2026-09-24 |
|
||||
| LESSON-20260926-failclosed-bypass-contract | IDOR、旁路、fail-open、白名单前缀、sh -c、validated_only、untracked、table_owners | 旁路入口必须继承主路径硬边界+SQL 防御纵深;门禁/授权器/执行器对未知默认拒绝;安全契约由执行器硬拒不依赖注释 | validated | [旁路与未知即拒绝契约](2026-09-26-failclosed-bypass-contract.md);2026-09-26 |
|
||||
|
||||
validated 的证据范围见各条目;本日复核为历史材料和源码阅读,未重跑产品回归。后续按机制补充,过时条目标 superseded/retired 并保留替代链接。
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "系统全面 Code Review(仅报告)"
|
||||
task_id: "system-code-review-2026-09-26"
|
||||
status: done
|
||||
created: 2026-09-26
|
||||
updated: 2026-09-26
|
||||
owner: Cursor
|
||||
task_type: research
|
||||
task_tier: small
|
||||
policy_revision: "agent-governance-2026-09-21-r1 + cursor-governance-2026-09-24-r1 @ f4c1b73f"
|
||||
branch: feature/system-code-review-2026-09-26
|
||||
worktree: .worktrees/system-code-review-2026-09-26
|
||||
base_dev_sha: "f4c1b73f28580ad3df6db4f9ec0a80057006e516"
|
||||
write_scope: [".doc"]
|
||||
allowed_environments: []
|
||||
head_sha: "f4c1b73f28580ad3df6db4f9ec0a80057006e516"
|
||||
mutex: "docs-audit-review"
|
||||
review_state: accepted
|
||||
integration_state: integrated
|
||||
deployment: not_required
|
||||
cleanup: done
|
||||
cleanup_owner: "独立集成/维护"
|
||||
cleanup_trigger: "报告合入 local_dev 且无进程占用本 worktree"
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# 系统全面 Code Review(仅报告)
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- **用户目标**:对系统做一次全面 code review,并按规范形成报告;不允许实施修复,只形成报告。
|
||||
- **交付终点**:审计主文档可审;INDEX 挂接;本任务记录收口。
|
||||
- **禁止项**:业务代码修复、gap-matrix/MATRIX 状态冒充发布、未授权环境写、push。
|
||||
|
||||
## 范围与非目标
|
||||
|
||||
- 允许写入:`.doc/audit`、`.doc/ops/short-term`、审计 INDEX。
|
||||
- 非目标:实施 FIND-* 修复、动态渗透、依赖 CVE。
|
||||
|
||||
## 相关问题与经验
|
||||
|
||||
- 检索词与结论见 [审计主文档](../../audit/2026-09-26-system-code-review.md)。
|
||||
- 无新增 lesson(未修复验证)。
|
||||
|
||||
## 计划与进展
|
||||
|
||||
1. [x] catalog overlap(none)+ worktree/register/claim
|
||||
2. [x] 四轴并行审查 + arch-check/规模盘点
|
||||
3. [x] 关键发现复核(IDOR、scan 未登记表、Confirm×proposed)
|
||||
4. [x] 落盘审计报告与 INDEX
|
||||
5. [x] 合入 local_dev(integrate-review-fixes 批次:报告+修复一起 --no-ff)
|
||||
6. [x] cleanup worktree(由修复批次维护者执行)
|
||||
|
||||
## 验证
|
||||
|
||||
- 研究验收 = 报告有据可审(FIND 带路径与置信度)。
|
||||
- 不适用产品 PI 全链路;**NOT REQUIRED** 产品测试 PASS。
|
||||
|
||||
## 状态与交接
|
||||
|
||||
- `status=done`(研究完成 ≠ 缺陷已修)。
|
||||
- 后续 P0 候选:FIND-SEC-01、FIND-ARCH-01(须新授权/新 task)。
|
||||
- cleanup 待合入后由独立维护者执行。
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: "系统 Code Review P0/P1 修复(2C+3H)"
|
||||
task_id: "system-review-p0-p1-fixes-2026-09-26"
|
||||
status: in_progress
|
||||
created: 2026-09-26
|
||||
updated: 2026-09-26
|
||||
owner: pi
|
||||
task_type: implementation
|
||||
task_tier: cross-module
|
||||
policy_revision: "agent-governance-2026-09-21-r1 @ f4c1b73f"
|
||||
branch: fix/system-review-p0-p1-fixes-2026-09-26
|
||||
worktree: .worktrees/system-review-p0-p1-fixes-2026-09-26
|
||||
base_dev_sha: "f4c1b73f28580ad3df6db4f9ec0a80057006e516"
|
||||
write_scope: ["internal/server", "internal/ctxmgr", "internal/authz", "internal/swboot", "internal/archtest", "internal/app/intentflow", "internal/intent", "internal/app/app.go", ".doc"]
|
||||
allowed_environments: []
|
||||
mutex: "review-fixes-batch"
|
||||
review_state: pending
|
||||
integration_state: pending
|
||||
deployment: pending
|
||||
cleanup: pending
|
||||
remote_sync: not_requested
|
||||
---
|
||||
|
||||
# 系统 Code Review P0/P1 修复
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- **来源**:[2026-09-26 系统 code review](../../audit/2026-09-26-system-code-review.md) 发现项。
|
||||
- **用户授权**(2026-09-26):一并修复 2 Critical(FIND-SEC-01、FIND-ARCH-01)+ 3 个 High 行(FIND-SEC-02/03、FIND-FLOW-01、FIND-QUAL-01);修复完成后与审计报告**一起提交**(同一批集成);完成后清理 worktree 任务分支。
|
||||
- **禁止项**:未授权环境写、push、晋升 test/main;FIND-ARCH-02(server Pool 接线)未在授权修复范围(ISSUES 登记 open)。
|
||||
|
||||
## 范围
|
||||
|
||||
- 修复主记录(含逐项根因/方案/回归):[remediation/2026-09-26-system-code-review-fixes.md](../../remediation/2026-09-26-system-code-review-fixes.md)。
|
||||
- Issue 台账:ISSUE-20260926-ctx-idor / archcheck-untracked / authz-failopen / swboot-shell-prefix / intentflow-validated-confirm / dual-flags-drift(resolved)+ governance-multi-entry(deferred)+ server-pool-wiring(open)。
|
||||
- 经验:LESSON-20260926-failclosed-bypass-contract(validated)。
|
||||
|
||||
## 问题检索与采纳
|
||||
|
||||
检索词:`IDOR`、`GetUserContext`、`session_id`、`validated_only`、`table_owners`、`untracked`、`fail-open`、`sh -c`、`前缀白名单`、`PlanTier`。命中:来源审计 FIND-*(全部);ISSUES 无同题已关闭条目;经验库无同机制 lesson(本轮新建)。采纳检查:模块规范 §2.1/§4(表唯一写者、端口装配)、验证契约「问题修复的回归范围」(原例+同类+正常行为)。
|
||||
|
||||
## 计划与进展
|
||||
|
||||
1. [x] worktree + 登记(base f4c1b73f)
|
||||
2. [x] 六项修复实施 + 回归用例
|
||||
3. [x] 验证:arch-check block / vet / short 全量 / 隔离 PG(IDOR PG、authz、duty);两个基线既有失败已核对非本批回归
|
||||
4. [x] 文档:修复主记录、ISSUES、lesson、索引
|
||||
5. [x] 提交
|
||||
6. [x] 集成(含审计报告分支同批 --no-ff,候选 integrate-review-fixes)
|
||||
7. [ ] 部署核验(dev 共享服务重启 + health/ready/业务行为——需另授权或独立部署任务)
|
||||
8. [ ] 清理(本任务 + 审计任务 worktree/分支)
|
||||
|
||||
## 验证
|
||||
|
||||
详见[修复主记录验收汇总](../../remediation/2026-09-26-system-code-review-fixes.md#验收汇总)。要点:arch-check block PASS(fail-closed 后)、vet/short 全绿、隔离 PG(55432 verify 容器临时库)IDOR/authz/duty PASS;`TestHealProjectDocuments_SkipsDutyLocked_PG` 与 PI E2E(KB 离线)为基线既有失败(stash 对照证实)。
|
||||
|
||||
## 状态与交接
|
||||
|
||||
- 集成后:任务状态改 done;审计任务(system-code-review-2026-09-26)的 integration/cleanup 由本批一并推进。
|
||||
- 部署:`deployment=pending`(代码影响运行内容;dev 服务重启与核验未在本任务授权内,见修复主记录)。
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: "系统 Code Review P0/P1 修复(IDOR、门禁盲区、fail-open、命令白名单、validated_only 硬拒)"
|
||||
status: done
|
||||
created: 2026-09-26
|
||||
updated: 2026-09-26
|
||||
owner: pi
|
||||
task_id: "system-review-p0-p1-fixes-2026-09-26"
|
||||
severity: critical
|
||||
source_audit: "../audit/2026-09-26-system-code-review.md"
|
||||
base_dev_sha: "f4c1b73f28580ad3df6db4f9ec0a80057006e516"
|
||||
---
|
||||
|
||||
# 系统 Code Review P0/P1 修复
|
||||
|
||||
来源:[2026-09-26 系统全面 code review](../audit/2026-09-26-system-code-review.md)(被审 tip `f4c1b73f`)。
|
||||
用户授权(2026-09-26):修复 2 Critical(FIND-SEC-01、FIND-ARCH-01)+ 3 个 High 行(FIND-SEC-02/03、FIND-FLOW-01、FIND-QUAL-01),修复后与报告一起集成,清理任务 worktree。本文件是下列 issue 的唯一主记录;发现细节与证据链以来源审计为准,此处记修复方案、经过与验收。
|
||||
|
||||
## 修复范围与 issue 映射
|
||||
|
||||
| FIND | issue_id | 修复内容 | 状态 |
|
||||
|---|---|---|---|
|
||||
| FIND-SEC-01 | ISSUE-20260926-ctx-idor | GetUserContext 会话归属校验(handler 403 + SQL/内存双防线) | resolved |
|
||||
| FIND-ARCH-01 | ISSUE-20260926-archcheck-untracked | arch-check 未登记表 fail-closed + 补登记 50 表 | resolved |
|
||||
| FIND-SEC-02 | ISSUE-20260926-authz-failopen | Authorizer 无池严格拒绝;显式 NewInMemoryAuthorizer 接缝 | resolved |
|
||||
| FIND-SEC-03 | ISSUE-20260926-swboot-shell-prefix | argv 化执行(去 sh -c)+ 白名单精确匹配 | resolved |
|
||||
| FIND-FLOW-01 | ISSUE-20260926-intentflow-validated-confirm | Confirm 拒 validated_only + 执行器硬读 mode | resolved |
|
||||
| FIND-QUAL-01 | ISSUE-20260926-dual-flags-drift | 组合根 flag 一致性 fail-fast | resolved(含残差) |
|
||||
| FIND-QUAL-01(入口收敛残差) | ISSUE-20260926-governance-multi-entry | docgov 三入口 + Coordinator/OwnerMux 合并 | deferred |
|
||||
| FIND-ARCH-02 | ISSUE-20260926-server-pool-wiring | server 持 Pool 直装 swboot(本轮未授权修复) | open |
|
||||
|
||||
## 各项修复经过
|
||||
|
||||
### 1. FIND-SEC-01 会话上下文 IDOR(ISSUE-20260926-ctx-idor)
|
||||
|
||||
- **根因**:`handleGetUserContext` 只校验 `user_id` 归属,`session_id` 直通 `GetUserContext`;`chat_messages` 查询 `WHERE session_id = $1` 无归属条件,绕过 chatflow 的会话授权。
|
||||
- **修复**(双层):
|
||||
- `internal/server/handlers_context.go`:sessionID 非空时经 `GetSessionOwnerID` 校验归属(不存在与归属他人同返 403,不泄露存在性)。
|
||||
- `internal/ctxmgr/manager.go`:SQL 增加 `EXISTS(chat_sessions 归属目标用户)` 子句;内存分支按 `sessions[sessionID].UserID == userID` 过滤——未来调用方遗漏 handler 校验也不泄露。
|
||||
- **回归**:`TestGetUserContext_OtherUserSessionDenied`(他户 session→403、不存在→403、本人→200 且含正文,内存 HTTP);`TestGetUserContext_PG_SessionOwnerFilter`(隔离 PG 库:他人视角 manager 层不返回正文、归属者正常)。
|
||||
|
||||
### 2. FIND-ARCH-01 arch-check 未登记表静默跳过(ISSUE-20260926-archcheck-untracked)
|
||||
|
||||
- **根因**:`internal/archtest/scan.go` `if !tracked { continue }`——表唯一写者门禁对未登记表(含 `swboot_controlled_executions`、`projects`、`outbox_events` 等)无强制力。
|
||||
- **修复**:
|
||||
- scan.go:未登记表产生 `sql_write` finding(fail-closed);仅豁免扫描正则已知假阳性词汇(`set/of/skip/below/row/state/step/doc/docgov/message/project/aggregation/ref_count`,来自 `ON CONFLICT DO UPDATE SET`、`FOR UPDATE OF/SKIP LOCKED` 与注释散文;静态清单,非策略豁免)。
|
||||
- `module-boundaries.yaml`:补登记 50 张既有写者真实表。多 owner 条目(`projects`、`users`、`project_members`、`context_memory_chunks`)是现状快照,新包写入仍被 block;单一写者收敛留在各模块批次(见 ISSUES 残差说明)。
|
||||
- **回归**:`TestUntrackedTableWriteIsBlocked`(新表写入必须 block 且 detail 提示登记);`TestUpsertSetKeywordIsNotFlaggedAsUntracked`(SET 噪音不误报)。全仓 `arch-check --mode block` PASS。
|
||||
|
||||
### 3. FIND-SEC-02 Authorizer 无池 fail-open(ISSUE-20260926-authz-failopen)
|
||||
|
||||
- **根因**:`IsProjectMember/IsProjectOwner` 在 `!enabled()`(pool==nil)时 `return userID > 0` ——任意登录用户获得任意项目访问;组合根始终 `NewAuthorizer(pool)`,nil pool 也构造成功,绕过「nil→failClosed」语义。
|
||||
- **修复**:`NewAuthorizer(nil)` 一律严格拒绝(false);新增显式 `NewInMemoryAuthorizer()` 仅供 ALLOW_IN_MEMORY 演示/测试接缝(视任意登录用户为成员);组合根 `app.Assemble` 在 `pool==nil && cfg.AllowInMemory` 时选用。误配(无库且未显式声明)→ 拒绝。
|
||||
- **波及与处置**:8 个依赖 fail-open 作测试接缝/隐式放行的既有用例——1 个改用显式内存授权 override,5 个业务行为用例改走 `newServerWithMemoryIAM`/`AllowInMemory=true`(显式演示模式契约),1 组 u5 用例补 `ModeLive`(属 FLOW-01 波及),1 个生命周期用例由组合根修复自然恢复。无削弱鉴权断言的用例。
|
||||
- **回归**:`TestMembershipChecks_FailClosedWithoutPool`;隔离 PG 库 authz 套件(ConcurrentLastWriter/DocumentDuty/AtomicDutyRevoke/UpsertProjectMember)PASS。
|
||||
|
||||
### 4. FIND-SEC-03 swboot sh -c + 前缀白名单(ISSUE-20260926-swboot-shell-prefix)
|
||||
|
||||
- **根因**:受控命令经 `sh -c cmdLine` 执行;允许判定为 basename 前缀匹配(`go` 放行 `go-evil`)。ApproveMerge 自身恒拒未变。
|
||||
- **修复**:`runCommand` 改 argv 化 `exec.CommandContext(fields[0], fields[1:]...)`(元字符只成为字面参数);`commandMatchesPrefixes`→`commandAllowed`,仅 basename 精确相等(含绝对路径条目取其 basename)。admin 白名单粒度仍是真实边界(报告 FIND-SEC-03 说明),运维登记白名单时应只放具体命令名。
|
||||
- **回归**:`TestExecutor_CommandAllowlistIsExactNotPrefix`(前缀伪装拒绝、精确/绝对路径放行、空命令拒绝);`TestExecutor_NoShellEvaluation`(`echo x && touch <marker>` 不求值、marker 不产生、输出为字面参数)。
|
||||
|
||||
### 5. FIND-FLOW-01 validated_only 可 Confirm 后 dispatch(ISSUE-20260926-intentflow-validated-confirm)
|
||||
|
||||
- **根因**:`DecisionConfirm` 允许 `PlanProposed`;确认后 HTTP 即 `DispatchEligible`;`ExecuteClaimed` 不读 `ExecutionMode`——shadow「安全」依赖「没人点 Confirm」。
|
||||
- **修复**(双层):
|
||||
- `DecidePlan`:Confirm 对 `plan.ExecutionMode == validated_only` 一律 `ErrPlanModeNotConfirmable`(403)。
|
||||
- `ExecuteClaimed`:硬读步骤级(缺省计划级)ExecutionMode,`validated_only` 一律 `ErrPlanModeNotExecutable`——覆盖所有派发路径(confirm 后、worker、误推进)。
|
||||
- 新错误码 `PLAN_MODE_NOT_CONFIRMABLE` / `PLAN_MODE_NOT_EXECUTABLE` 入 `ErrorCode` 与 HTTP 映射。
|
||||
- **回归**:`TestConfirmRejectedForValidatedOnlyPlan`;`TestExecuteClaimedRefusesValidatedOnlyStep`(经 DecisionContinue 误推进后 DispatchEligible 硬拒,步骤不成 succeeded、owner 零执行)。既有 4 个用例的 `draft()` helper 补显式 `ModeLive`(缺省 validated_only 是安全默认,live 机制用例需显式声明)。
|
||||
|
||||
### 6. FIND-QUAL-01 双套 flags 静默漂移(ISSUE-20260926-dual-flags-drift,含残差)
|
||||
|
||||
- **修复**:`intentflow.ValidateFlagConsistency`(组合根 `Assemble` 调用)——`PlanTier` 升档但 `MULTI_INTENT_ENABLED=false` 时启动期 fail-fast(`Assemble` 最早阶段调用),消除「intentflow 已升档、intent 仍关」静默漂移配置上线。
|
||||
- **回归**:`TestValidateIntentFlagConsistency`(升档+开=过;legacy 恒过;四档×关=拒)。
|
||||
- **残差(deferred,ISSUE-20260926-governance-multi-entry)**:docgov 三入口(chat PendingAction / docgov_owner / handlers_docgov)与 Coordinator ~40 intent case + OwnerMux 平行面合并是多日级重构(自带回归矩阵),超出本批授权合理范围;已在 ISSUES 登记 owner/触发条件。
|
||||
|
||||
## 验收汇总
|
||||
|
||||
被测 SHA:本任务 worktree HEAD(见下方提交记录);基线 `f4c1b73f`。
|
||||
|
||||
| 检查 | 结果 |
|
||||
|---|---|
|
||||
| `./scripts/arch-check.sh --mode block` | PASS(fail-closed 改动 + 50 表登记后) |
|
||||
| `go vet ./...` | PASS |
|
||||
| `go test -short ./...` | 全绿 |
|
||||
| 隔离 PG:`TestGetUserContext_PG_SessionOwnerFilter` | PASS(临时库建删于 55432 verify 容器) |
|
||||
| 隔离 PG:authz + server duty 套件 | PASS |
|
||||
| 隔离 PG:duty-sync 脚本 | `TestHealProjectDocuments_SkipsDutyLocked_PG` FAIL——**基线 f4c1b73f 同样失败**(FK fixture 既有问题,与本批无关) |
|
||||
| `TestPiAgent_FullLifecycle_MultiRole_RealWorld_E2E` | 非 short 模式因 KB 离线前置检查失败——**基线同样失败**(需真实 Runner/KB,非本批回归) |
|
||||
|
||||
## 部署
|
||||
|
||||
代码已按授权集成 local_dev(见任务记录)。dev 共享服务未在本任务内重建/重启(未获 env 写授权);`deployment=pending`,重启与 `/health`、`/ready`、业务行为核验待部署任务执行。
|
||||
|
||||
## 经验
|
||||
|
||||
见 [LESSON-20260926-failclosed-bypass-contract](../ops/lessons/2026-09-26-failclosed-bypass-contract.md):旁路入口必须继承主路径硬边界;门禁/执行器对「未知」默认必须拒绝。
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [2026-09-26-system-code-review-fixes.md](./2026-09-26-system-code-review-fixes.md) | 系统 code review P0/P1 修复主记录:IDOR、arch-check 未登记表 fail-closed(补登 50 表)、authz 无池 fail-open、swboot argv 化+精确白名单、intentflow validated_only 双层硬拒、flag 一致性 fail-fast;配套 [审计](../audit/2026-09-26-system-code-review.md) · [经验](../ops/lessons/2026-09-26-failclosed-bypass-contract.md) |
|
||||
| [2026-09-25-dialogue-office-p0-verification.md](./2026-09-25-dialogue-office-p0-verification.md) | 对话办公 P0 验证记录:假成功消除单测 + arch-check;PI 补证待授权 |
|
||||
| [2026-09-25-dialogue-office-expansion-plan.md](./2026-09-25-dialogue-office-expansion-plan.md) | 对话办公能力扩展 P0–P3:消除主链假成功、intentflow dry-run live、查询真数据源、新写意图(trace/补充确认/批注/资产等);治理结构 UI 专属。配套 [调研](../audit/2026-09-25-dialogue-office-capability-research.md)。planned,不实施代码 |
|
||||
| [2026-09-23-coordination-db-daemon-implementation-undertaking-plan.md](./2026-09-23-coordination-db-daemon-implementation-undertaking-plan.md) | 协调库与 daemon **实施承接计划**(工作包预填):W1–W3 七包(U1r/U2/U3/U7/U4/U5/U6)含符号级锚点、真实并发验收、mutex、开工准入;L2 仅 ADR 前置门。配套 [设计方案](./2026-09-23-coordination-db-daemon-plan.md) · [任务](../ops/short-term/2026-09-23-coordination-db-daemon-impl-plan.md)。planned,不实施代码 |
|
||||
|
||||
@@ -465,6 +465,115 @@ table_owners:
|
||||
- sysadmin
|
||||
maintenance_windows:
|
||||
- maintenance
|
||||
# FIND-ARCH-01 回归补登记(2026-09-26):以下为既有写者的真实表。
|
||||
# 多 owner 条目是现状快照(历史多写者),新包写入仍会被 block;
|
||||
# 单一写者收敛在各模块整改批次处理,不在本批扩大。
|
||||
agentx_workspace_registry:
|
||||
- workspace
|
||||
asset_parses:
|
||||
- materialindex
|
||||
auth_sessions:
|
||||
- auth
|
||||
chat_read_positions:
|
||||
- ctxmgr
|
||||
chat_sessions:
|
||||
- ctxmgr
|
||||
context_memory_chunks:
|
||||
- ctxmgr
|
||||
- storage
|
||||
document_access_audit:
|
||||
- freedoc
|
||||
document_agent_candidates:
|
||||
- docrevision
|
||||
document_audit_logs:
|
||||
- docgov
|
||||
document_change_proposals:
|
||||
- docgov
|
||||
document_change_reports:
|
||||
- docrevision
|
||||
document_comments:
|
||||
- docgov
|
||||
document_content_blobs:
|
||||
- docrevision
|
||||
document_content_projections:
|
||||
- docrevision
|
||||
document_duty_assignments:
|
||||
- projectinstance
|
||||
document_lineage:
|
||||
- projectworkflow
|
||||
document_pending_supplements:
|
||||
- docgov
|
||||
file_references:
|
||||
- storage
|
||||
freeform_document_versions:
|
||||
- freedoc
|
||||
iam_system_config:
|
||||
- auth
|
||||
material_chunks:
|
||||
- materialindex
|
||||
outbox_events:
|
||||
- notification
|
||||
project_activities:
|
||||
- ctxmgr
|
||||
project_blackboards:
|
||||
- ctxmgr
|
||||
project_context_snapshot_sources:
|
||||
- projectcontext
|
||||
project_context_snapshots:
|
||||
- projectcontext
|
||||
project_doc_task_instances:
|
||||
- projectinstance
|
||||
project_members:
|
||||
- authz
|
||||
- docduty
|
||||
project_source_bindings:
|
||||
- projectworkflow
|
||||
project_stage_instances:
|
||||
- projectinstance
|
||||
project_workflow_instances:
|
||||
- projectinstance
|
||||
project_workflows:
|
||||
- projectworkflow
|
||||
projects:
|
||||
- ctxmgr
|
||||
- docgov
|
||||
- projectinstance
|
||||
- projectworkflow
|
||||
storage_blobs:
|
||||
- storage
|
||||
swboot_controlled_executions:
|
||||
- swboot
|
||||
system_role_permissions:
|
||||
- authz
|
||||
system_roles:
|
||||
- authz
|
||||
task_run_events:
|
||||
- taskrun
|
||||
template_assets:
|
||||
- workflowdef
|
||||
template_versions:
|
||||
- workflowdef
|
||||
user_credentials:
|
||||
- auth
|
||||
user_password_reset_audits:
|
||||
- auth
|
||||
user_preferences:
|
||||
- ctxmgr
|
||||
user_role_bindings:
|
||||
- auth
|
||||
users:
|
||||
- auth
|
||||
- docgov
|
||||
workflow_activation_audits:
|
||||
- projectworkflow
|
||||
workflow_activation_tokens:
|
||||
- projectinstance
|
||||
workflow_definitions:
|
||||
- workflowdef
|
||||
workflow_runs:
|
||||
- projectworkflow
|
||||
workflow_versions:
|
||||
- workflowdef
|
||||
|
||||
path_owners:
|
||||
output.md:
|
||||
|
||||
@@ -110,6 +110,10 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) {
|
||||
if cfg != nil {
|
||||
observability.InitLogger(cfg.Env)
|
||||
}
|
||||
// FIND-QUAL-01:组合根启动期校验双套 flag 一致性(fail-fast)。
|
||||
if err := intentflow.ValidateFlagConsistency(intent.CurrentFeatureFlags(), intentflow.CurrentFeatureFlags()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if svc == nil {
|
||||
var err error
|
||||
svc, err = bizdev.NewService(cfg)
|
||||
@@ -265,6 +269,11 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) {
|
||||
}
|
||||
|
||||
authorizer := authz.NewAuthorizer(pool)
|
||||
if pool == nil && cfg != nil && cfg.AllowInMemory {
|
||||
// 显式 ALLOW_IN_MEMORY 演示模式:无库时采用内存授权语义;
|
||||
// 误配(无库且未显式声明)仍走 NewAuthorizer(nil) 的严格拒绝。
|
||||
authorizer = authz.NewInMemoryAuthorizer()
|
||||
}
|
||||
wireDutySlotWriter(authorizer)
|
||||
rt := &Runtime{
|
||||
Cfg: cfg,
|
||||
|
||||
@@ -12,6 +12,12 @@ var (
|
||||
ErrPlanActionForbidden = errors.New("PLAN_ACTION_FORBIDDEN")
|
||||
ErrPlanActionVersionConflict = errors.New("PLAN_ACTION_VERSION_CONFLICT")
|
||||
ErrUnauthenticated = errors.New("ERR_UNAUTHENTICATED")
|
||||
// FIND-FLOW-01 回归:validated_only(shadow/dry-run)计划的硬边界。
|
||||
// Confirm 不得把 validated_only 计划推入 executing;执行器也不得
|
||||
// 对 validated_only 步骤做真实副作用——安全契约由代码硬拒,不依赖
|
||||
//「没人点 Confirm」。
|
||||
ErrPlanModeNotConfirmable = errors.New("PLAN_MODE_NOT_CONFIRMABLE")
|
||||
ErrPlanModeNotExecutable = errors.New("PLAN_MODE_NOT_EXECUTABLE")
|
||||
)
|
||||
|
||||
var errTurnExists = errors.New("intentflow: turn exists")
|
||||
@@ -33,6 +39,10 @@ func ErrorCode(err error) string {
|
||||
return "PLAN_ACTION_FORBIDDEN"
|
||||
case errors.Is(err, ErrPlanActionVersionConflict):
|
||||
return "PLAN_ACTION_VERSION_CONFLICT"
|
||||
case errors.Is(err, ErrPlanModeNotConfirmable):
|
||||
return "PLAN_MODE_NOT_CONFIRMABLE"
|
||||
case errors.Is(err, ErrPlanModeNotExecutable):
|
||||
return "PLAN_MODE_NOT_EXECUTABLE"
|
||||
case errors.Is(err, ErrUnauthenticated):
|
||||
return "ERR_UNAUTHENTICATED"
|
||||
case errors.Is(err, ErrSegmentProposalDisabled):
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package intentflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"chat-agent-system/internal/intent"
|
||||
)
|
||||
|
||||
// PlanTier is the intentflow rollout switch. It must not be stored on
|
||||
@@ -123,3 +126,17 @@ func tierRank(t PlanTier) int {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateFlagConsistency 校验 intentflow 放量档位与 intent 多意图开关的
|
||||
// 一致性(FIND-QUAL-01 回归):PlanTier 升档必须建立在 MULTI_INTENT_ENABLED
|
||||
// 之上,否则组合根启动期 fail-fast,不允许「intentflow 已升档、intent 仍关」
|
||||
// 这类静默漂移配置上线。
|
||||
func ValidateFlagConsistency(iflags intent.FeatureFlags, pflags FeatureFlags) error {
|
||||
if pflags.PlanTier != TierLegacy && !iflags.MultiIntentEnabled {
|
||||
return fmt.Errorf(
|
||||
"intentflow: MULTI_INTENT_PLAN_TIER=%q requires MULTI_INTENT_ENABLED=true; fix the flag combination instead of silently degrading",
|
||||
pflags.PlanTier,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package intentflow
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/intent"
|
||||
)
|
||||
|
||||
func TestDefaultPlanTierIsLegacy(t *testing.T) {
|
||||
@@ -134,3 +136,21 @@ func TestRESTPathsFrozen(t *testing.T) {
|
||||
t.Fatal(PathPlanDecisions)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateFlagConsistency FIND-QUAL-01 回归:PlanTier 升档但多意图识别
|
||||
// 关闭时组合根必须 fail-fast,不允许静默漂移配置。
|
||||
func TestValidateFlagConsistency(t *testing.T) {
|
||||
base := intent.FeatureFlags{MultiIntentEnabled: true}
|
||||
if err := ValidateFlagConsistency(base, FeatureFlags{PlanTier: TierFull}); err != nil {
|
||||
t.Fatalf("elevated tier with multi-intent on should pass: %v", err)
|
||||
}
|
||||
if err := ValidateFlagConsistency(base, FeatureFlags{PlanTier: TierLegacy}); err != nil {
|
||||
t.Fatalf("legacy tier should always pass: %v", err)
|
||||
}
|
||||
drifted := intent.FeatureFlags{MultiIntentEnabled: false}
|
||||
for _, tier := range []PlanTier{TierShadow, TierAutoRead, TierConfirm, TierFull} {
|
||||
if err := ValidateFlagConsistency(drifted, FeatureFlags{PlanTier: tier}); err == nil {
|
||||
t.Fatalf("tier %s with multi-intent off must fail fast", tier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +364,11 @@ func (p *Plans) DecidePlan(ctx context.Context, in DecisionCommand) (*PlanView,
|
||||
switch in.Kind {
|
||||
case DecisionConfirm, DecisionConfirmRetry, DecisionContinue:
|
||||
if in.Kind == DecisionConfirm {
|
||||
// FIND-FLOW-01 回归:validated_only(shadow/dry-run)计划不允许
|
||||
// 经 Confirm 进入 executing;shadow 安全不依赖「没人点 Confirm」。
|
||||
if plan.ExecutionMode == ModeValidatedOnly {
|
||||
return ErrPlanModeNotConfirmable
|
||||
}
|
||||
if plan.Status != PlanAwaitingConfirmation && plan.Status != PlanAwaitingClarification && plan.Status != PlanProposed {
|
||||
return ErrPlanStaleRevision
|
||||
}
|
||||
@@ -618,6 +623,16 @@ func (p *Plans) ExecuteClaimed(ctx context.Context, claimed *ClaimedStep) (*Plan
|
||||
if p.owner == nil {
|
||||
return nil, ErrPlanActionForbidden
|
||||
}
|
||||
// FIND-FLOW-01 回归:执行器硬读 ExecutionMode——validated_only 步骤
|
||||
// 一律拒绝真实执行,无论上游状态机如何推进。步骤级 mode 优先,
|
||||
// 缺省回退计划级。
|
||||
effectiveMode := claimed.Plan.ExecutionMode
|
||||
if claimed.Step.ExecutionMode != "" {
|
||||
effectiveMode = claimed.Step.ExecutionMode
|
||||
}
|
||||
if effectiveMode == ModeValidatedOnly {
|
||||
return nil, ErrPlanModeNotExecutable
|
||||
}
|
||||
exec := StepExec{
|
||||
Principal: claimed.Plan.Principal,
|
||||
PlanID: claimed.Plan.PlanID,
|
||||
|
||||
@@ -40,6 +40,9 @@ func draft(digest string) intentflow.CreateCommand {
|
||||
RequestDigest: digest,
|
||||
WritePlan: true,
|
||||
SafeSummary: "query then generate",
|
||||
// 这些用例验证 live 确认/派发机制;CreateCommand 缺省 mode 是
|
||||
// validated_only(安全默认,不可 Confirm),必须显式声明 live。
|
||||
ExecutionMode: intentflow.ModeLive,
|
||||
Steps: []intentflow.StepDraft{
|
||||
{ActionKey: "regulation.query", ActionDigest: "q1"},
|
||||
{ActionKey: "task.run", ActionDigest: "w1"},
|
||||
@@ -319,3 +322,73 @@ func TestUnauthenticatedRejected(t *testing.T) {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfirmRejectedForValidatedOnlyPlan FIND-FLOW-01 回归:
|
||||
// validated_only(shadow)的 proposed 计划不允许经 Confirm 进入 executing。
|
||||
func TestConfirmRejectedForValidatedOnlyPlan(t *testing.T) {
|
||||
p, _, _ := testPlans()
|
||||
ctx := context.Background()
|
||||
cmd := draft("digest-vo")
|
||||
cmd.Status = intentflow.PlanProposed
|
||||
cmd.ExecutionMode = intentflow.ModeValidatedOnly
|
||||
created, err := p.CreatePlan(ctx, cmd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = p.DecidePlan(ctx, intentflow.DecisionCommand{
|
||||
Principal: principal(),
|
||||
PlanID: created.PlanID,
|
||||
ExpectedRevision: created.Revision,
|
||||
DecisionNonce: "n-vo",
|
||||
DecisionDigest: "d-vo",
|
||||
Kind: intentflow.DecisionConfirm,
|
||||
})
|
||||
if !errors.Is(err, intentflow.ErrPlanModeNotConfirmable) {
|
||||
t.Fatalf("confirm on validated_only plan must be rejected with ErrPlanModeNotConfirmable, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteClaimedRefusesValidatedOnlyStep FIND-FLOW-01 回归:
|
||||
// 执行器硬读 ExecutionMode——validated_only 计划即便被误推进到 executing
|
||||
// 并经 DispatchEligible 派发,也拒绝真实执行(同 http-confirm 派发入口)。
|
||||
func TestExecuteClaimedRefusesValidatedOnlyStep(t *testing.T) {
|
||||
p, _, owner := testPlans()
|
||||
ctx := context.Background()
|
||||
cmd := draft("digest-vo-exec")
|
||||
// 直接构造「上游状态机被误推进」的形状:executing + validated_only。
|
||||
cmd.Status = intentflow.PlanExecuting
|
||||
cmd.ExecutionMode = intentflow.ModeValidatedOnly
|
||||
created, err := p.CreatePlan(ctx, cmd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// DecisionContinue 允许 executing 计划推进(applyConfirm 将步骤置为
|
||||
// eligible),模拟状态机被误推进后的派发路径。
|
||||
_, err = p.DecidePlan(ctx, intentflow.DecisionCommand{
|
||||
Principal: principal(),
|
||||
PlanID: created.PlanID,
|
||||
ExpectedRevision: created.Revision,
|
||||
DecisionNonce: "n-vo-exec",
|
||||
DecisionDigest: "d-vo-exec",
|
||||
Kind: intentflow.DecisionContinue,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = p.DispatchEligible(ctx, "worker-vo", created.PlanID)
|
||||
if !errors.Is(err, intentflow.ErrPlanModeNotExecutable) {
|
||||
t.Fatalf("dispatch of validated_only plan must hard-refuse with ErrPlanModeNotExecutable, got %v", err)
|
||||
}
|
||||
plan, err := p.GetPlan(ctx, principal(), created.PlanID)
|
||||
if err != nil || plan == nil {
|
||||
t.Fatalf("get plan: %v %v", plan, err)
|
||||
}
|
||||
for _, s := range plan.Steps {
|
||||
if s.Status == intentflow.StepSucceeded {
|
||||
t.Fatalf("validated_only step must never be written as succeeded: %+v", s)
|
||||
}
|
||||
if owner.ExecCount(s.StepID) != 0 {
|
||||
t.Fatalf("owner must not observe real execution for %s", s.StepID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package archtest
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -83,6 +84,40 @@ func TestOwnerProductionWriteIsAllowed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestUntrackedTableWriteIsBlocked FIND-ARCH-01 回归:未登记表的生产写
|
||||
// 不再被静默跳过,必须产生 sql_write finding(fail-closed)。
|
||||
func TestUntrackedTableWriteIsBlocked(t *testing.T) {
|
||||
var hit *Finding
|
||||
findings := scanMini(t)
|
||||
for i, f := range findings {
|
||||
if f.Kind == KindSQLWrite && f.Symbol == "brand_new_table" && f.Package == "server" {
|
||||
hit = &findings[i]
|
||||
if f.File != "internal/server/untracked_write.go" {
|
||||
t.Fatalf("expected untracked_write.go, got %s", f.File)
|
||||
}
|
||||
if !strings.Contains(f.Detail, "untracked table") {
|
||||
t.Fatalf("expected untracked-table detail, got %q", f.Detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
if hit == nil {
|
||||
t.Fatal("expected INSERT INTO brand_new_table in internal/server/untracked_write.go to be blocked as untracked")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpsertSetKeywordIsNotFlaggedAsUntracked 证明 ON CONFLICT DO UPDATE SET
|
||||
// 等正则假阳性词汇不会因 fail-closed 改动而误报。
|
||||
func TestUpsertSetKeywordIsNotFlaggedAsUntracked(t *testing.T) {
|
||||
for _, f := range scanMini(t) {
|
||||
if f.Kind == KindSQLWrite && f.Symbol == "set" {
|
||||
t.Fatalf("upsert SET keyword must stay exempt noise, got finding: %+v", f)
|
||||
}
|
||||
if f.Kind == KindSQLWrite && f.Package == "docgov" && f.File == "internal/docgov/owner_upsert_write.go" {
|
||||
t.Fatalf("owner upsert must stay allowed (incl. SET noise): %+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathJoinConcatAndConstAreDetected(t *testing.T) {
|
||||
want := map[string]bool{
|
||||
"internal/server/path_join.go": false,
|
||||
|
||||
@@ -18,6 +18,15 @@ var (
|
||||
eventNameRe = regexp.MustCompile(`(?:document|intent\.plan)\.[a-z0-9_.]+`)
|
||||
)
|
||||
|
||||
// sqlScanNoise 扫描正则的已知假阳性词汇:来自 SQL 子句(ON CONFLICT DO
|
||||
// UPDATE SET、FOR UPDATE OF/SKIP LOCKED)与注释散文,不是真实表名。
|
||||
// 它们不是策略豁免:新出现的未登记表仍会 fail-closed。
|
||||
var sqlScanNoise = map[string]bool{
|
||||
"set": true, "of": true, "skip": true, "below": true, "row": true,
|
||||
"state": true, "step": true, "doc": true, "docgov": true,
|
||||
"message": true, "project": true, "aggregation": true, "ref_count": true,
|
||||
}
|
||||
|
||||
type fileClass int
|
||||
|
||||
const (
|
||||
@@ -201,6 +210,17 @@ func scanSQLWrites(src, pkg, rel string, b *Boundaries) []Finding {
|
||||
table := strings.ToLower(m[2])
|
||||
owners, tracked := b.TableOwners[table]
|
||||
if !tracked {
|
||||
// FIND-ARCH-01 回归:未登记表不再静默跳过(fail-closed)。
|
||||
// 仅豁免扫描正则的已知假阳性词汇;真实新表必须登记 table_owners。
|
||||
if !sqlScanNoise[table] {
|
||||
out = append(out, Finding{
|
||||
Kind: KindSQLWrite,
|
||||
Package: pkg,
|
||||
File: rel,
|
||||
Symbol: table,
|
||||
Detail: fmt.Sprintf("write to untracked table %s: register it in module-boundaries.yaml table_owners", table),
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
if allowedHas(owners, pkg) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package docgov
|
||||
|
||||
// ownerUpsertNoise includes ON CONFLICT DO UPDATE SET prose that the regex
|
||||
// misreads as a table named "set"; it must stay exempt noise, not a finding,
|
||||
// and the owner write on document_records itself stays allowed.
|
||||
const ownerUpsertNoise = `
|
||||
INSERT INTO document_records (doc_id, stage) VALUES ($1, $2)
|
||||
ON CONFLICT (doc_id) DO UPDATE SET stage = EXCLUDED.stage
|
||||
`
|
||||
@@ -0,0 +1,7 @@
|
||||
package server
|
||||
|
||||
// brand_new_table is intentionally NOT registered in fixtureBoundaries
|
||||
// table_owners: writing it must produce an untracked-table finding.
|
||||
const untrackedSQL = `
|
||||
INSERT INTO brand_new_table (id, payload) VALUES ($1, $2)
|
||||
`
|
||||
@@ -62,12 +62,22 @@ type DutySlotWriter interface {
|
||||
type Authorizer struct {
|
||||
pool *pgxpool.Pool
|
||||
dutySlots DutySlotWriter
|
||||
// inMemory 显式声明「无库演示模式」:仅 NewInMemoryAuthorizer 构建。
|
||||
// NewAuthorizer(nil) 一律严格拒绝(FIND-SEC-02 回归),防止误配
|
||||
// (无库且未显式 ALLOW_IN_MEMORY)时任意登录用户获得项目访问。
|
||||
inMemory bool
|
||||
}
|
||||
|
||||
func NewAuthorizer(pool *pgxpool.Pool) *Authorizer {
|
||||
return &Authorizer{pool: pool}
|
||||
}
|
||||
|
||||
// NewInMemoryAuthorizer 仅供显式 ALLOW_IN_MEMORY 演示/测试模式:无库时
|
||||
// 视任意登录用户为项目成员(单用户演示语义)。生产/DB 路径禁止使用。
|
||||
func NewInMemoryAuthorizer() *Authorizer {
|
||||
return &Authorizer{inMemory: true}
|
||||
}
|
||||
|
||||
// SetDutySlotWriter injects the document_records slot clearer (docgov adapter).
|
||||
func (a *Authorizer) SetDutySlotWriter(w DutySlotWriter) {
|
||||
if a == nil {
|
||||
@@ -105,12 +115,14 @@ func (a *Authorizer) AddOwnerOnCreate(ctx context.Context, projectID, userID int
|
||||
}
|
||||
|
||||
// IsProjectMember 判断用户是否为项目 active 成员或创建人。
|
||||
// FIND-SEC-02 回归:无池(且非显式内存模式)时 fail-closed 返回 false,
|
||||
// 不再默认放行任意登录用户;与 GetProjectMember 的禁用语义对齐。
|
||||
func (a *Authorizer) IsProjectMember(ctx context.Context, projectID, userID int64) (bool, error) {
|
||||
if projectID <= 0 || userID <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
if !a.enabled() {
|
||||
return userID > 0, nil
|
||||
return a.inMemory && userID > 0, nil
|
||||
}
|
||||
var member bool
|
||||
err := a.pool.QueryRow(ctx, `
|
||||
@@ -198,12 +210,13 @@ func validateProjectRole(role string) error {
|
||||
}
|
||||
|
||||
// IsProjectOwner 判断用户是否为项目 owner(成员表或创建人)。
|
||||
// FIND-SEC-02 回归:无池时 fail-closed,同 IsProjectMember。
|
||||
func (a *Authorizer) IsProjectOwner(ctx context.Context, projectID, userID int64) (bool, error) {
|
||||
if projectID <= 0 || userID <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
if !a.enabled() {
|
||||
return userID > 0, nil
|
||||
return a.inMemory && userID > 0, nil
|
||||
}
|
||||
var owner bool
|
||||
err := a.pool.QueryRow(ctx, `
|
||||
|
||||
@@ -101,3 +101,21 @@ func TestGetProjectMember_DisabledWithoutPool(t *testing.T) {
|
||||
t.Fatalf("disabled authorizer must not invent a member, got (%v, %v)", rec, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMembershipChecks_FailClosedWithoutPool FIND-SEC-02 回归:
|
||||
// 无池 Authorizer 的成员/owner 判定必须 fail-closed,不得放行任意登录用户。
|
||||
func TestMembershipChecks_FailClosedWithoutPool(t *testing.T) {
|
||||
a := authz.NewAuthorizer(nil)
|
||||
for name, fn := range map[string]func() (bool, error){
|
||||
"IsProjectMember": func() (bool, error) { return a.IsProjectMember(context.Background(), 1, 2) },
|
||||
"IsProjectOwner": func() (bool, error) { return a.IsProjectOwner(context.Background(), 1, 2) },
|
||||
} {
|
||||
ok, err := fn()
|
||||
if err != nil || ok {
|
||||
t.Fatalf("%s without pool must fail closed (false, nil), got (%v, %v)", name, ok, err)
|
||||
}
|
||||
}
|
||||
if err := a.RequireProjectAccess(context.Background(), 1, 2); err == nil {
|
||||
t.Fatalf("RequireProjectAccess without pool must deny, got nil error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,15 +374,20 @@ func (m *ContextManager) GetUserContext(ctx context.Context, projectID, userID i
|
||||
AssembledAt: time.Now(),
|
||||
}
|
||||
|
||||
// 1. 获取近期会话
|
||||
// 1. 获取近期会话(FIND-SEC-01 防御纵深:SQL 层限定会话归属目标用户,
|
||||
// 即使未来新增调用方遗漏 handler 校验也不会泄露他人会话正文)
|
||||
if sessionID != "" {
|
||||
if m.pool != nil {
|
||||
rows, err := m.pool.Query(ctx, `
|
||||
SELECT id, session_id, COALESCE(user_id, 0), role, content, intent, branch, metadata, created_at
|
||||
FROM chat_messages
|
||||
WHERE session_id = $1
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM chat_sessions cs
|
||||
WHERE cs.session_id = $1 AND COALESCE(cs.user_id, 0) = $2
|
||||
)
|
||||
ORDER BY created_at ASC LIMIT 30
|
||||
`, sessionID)
|
||||
`, sessionID, userID)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
@@ -399,7 +404,9 @@ func (m *ContextManager) GetUserContext(ctx context.Context, projectID, userID i
|
||||
}
|
||||
} else {
|
||||
m.mu.RLock()
|
||||
userCtx.RecentChats = append(userCtx.RecentChats, m.messages[sessionID]...)
|
||||
if sess, ok := m.sessions[sessionID]; ok && sess.UserID == userID {
|
||||
userCtx.RecentChats = append(userCtx.RecentChats, m.messages[sessionID]...)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ import (
|
||||
func TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
// 隔离写权门闩:跳过项目成员库检查,只验证 AuthorID。禁止依赖共享库或默认拒绝。
|
||||
// FIND-SEC-02 后 NewAuthorizer(nil) 严格拒绝,改用显式内存授权接缝。
|
||||
srv.cfg.AllowInMemory = true
|
||||
srv.setOverrideAuthorizer(authz.NewAuthorizer(nil))
|
||||
srv.setOverrideAuthorizer(authz.NewInMemoryAuthorizer())
|
||||
ctx := context.Background()
|
||||
const (
|
||||
authorID int64 = 170
|
||||
|
||||
@@ -41,6 +41,19 @@ func (s *Server) handleGetUserContext(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// FIND-SEC-01 回归:session_id 必须归属于目标用户本人,否则拒绝。
|
||||
// 会话不存在与归属他人同样返回 403,避免泄露会话存在性。
|
||||
if sessionID != "" {
|
||||
ownerID, err := s.ctxMgr().GetSessionOwnerID(r.Context(), sessionID)
|
||||
if err != nil || ownerID <= 0 || ownerID != userID {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "ERR_FORBIDDEN",
|
||||
"message": "无权访问该会话上下文",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
uCtx, err := s.ctxMgr().GetUserContext(r.Context(), projectID, userID, sessionID)
|
||||
if err != nil {
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "get user context failed: "+err.Error())
|
||||
|
||||
@@ -2,9 +2,11 @@ package server_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -142,3 +144,125 @@ func TestChatRecategorize_RequiresOwnedSession(t *testing.T) {
|
||||
t.Fatalf("expected 200 for owned session, got %d %s", ownRec.Code, ownRec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetUserContext_OtherUserSessionDenied FIND-SEC-01 回归:
|
||||
// GetUserContext 旁路按 session_id 取消息必须校验会话归属。
|
||||
func TestGetUserContext_OtherUserSessionDenied(t *testing.T) {
|
||||
cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AdminUsername: "admin", AdminPassword: "admin-test-pass"}
|
||||
svc := bizdev.MustNewService(t, cfg)
|
||||
srv := server.NewServer(cfg, svc)
|
||||
handler := srv.Routes()
|
||||
adminToken := loginTestAdmin(t, handler, cfg)
|
||||
|
||||
suffix := time.Now().Format("150405.000")
|
||||
victimToken := createAndLoginUser(t, handler, adminToken, "ctx_victim_"+suffix, "pass12345")
|
||||
attackerToken := createAndLoginUser(t, handler, adminToken, "ctx_attacker_"+suffix, "pass12345")
|
||||
sessionID := "sess-ctx-idor-" + suffix
|
||||
|
||||
chatBody, _ := json.Marshal(map[string]any{
|
||||
"session_id": sessionID,
|
||||
"message": "victim-private-context-body",
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/chat", bytes.NewReader(chatBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
withBearerAuth(req, victimToken)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("victim chat: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// 攻击者用自己的 user_id + 受害者 session_id:必须 403。
|
||||
getReq := httptest.NewRequest(http.MethodGet, "/api/v1/context/user?session_id="+sessionID, nil)
|
||||
withBearerAuth(getReq, attackerToken)
|
||||
getRec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(getRec, getReq)
|
||||
if getRec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403 for other user's session context, got %d %s", getRec.Code, getRec.Body.String())
|
||||
}
|
||||
if strings.Contains(getRec.Body.String(), "victim-private-context-body") {
|
||||
t.Fatalf("response must not leak victim messages: %s", getRec.Body.String())
|
||||
}
|
||||
|
||||
// 不存在的会话同样 403(不泄露存在性)。
|
||||
ghostReq := httptest.NewRequest(http.MethodGet, "/api/v1/context/user?session_id=sess-ctx-idor-ghost", nil)
|
||||
withBearerAuth(ghostReq, attackerToken)
|
||||
ghostRec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(ghostRec, ghostReq)
|
||||
if ghostRec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403 for unknown session, got %d %s", ghostRec.Code, ghostRec.Body.String())
|
||||
}
|
||||
|
||||
// 会话归属者本人仍可正常取回上下文。
|
||||
ownReq := httptest.NewRequest(http.MethodGet, "/api/v1/context/user?session_id="+sessionID, nil)
|
||||
withBearerAuth(ownReq, victimToken)
|
||||
ownRec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(ownRec, ownReq)
|
||||
if ownRec.Code != http.StatusOK {
|
||||
t.Fatalf("owner context read failed: %d %s", ownRec.Code, ownRec.Body.String())
|
||||
}
|
||||
if !strings.Contains(ownRec.Body.String(), "victim-private-context-body") {
|
||||
t.Fatalf("owner should see own session messages: %s", ownRec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetUserContext_PG_SessionOwnerFilter FIND-SEC-01 PG 层防御纵深回归:
|
||||
// GetUserContext 的 chat_messages 查询必须按会话归属过滤,DB 层也不泄露
|
||||
// 他人会话正文(即使未来调用方遗漏 handler 校验)。
|
||||
func TestGetUserContext_PG_SessionOwnerFilter(t *testing.T) {
|
||||
pool := openPGPoolWithMigrations(t)
|
||||
if pool == nil {
|
||||
t.Skip("PG not available")
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
handler, srv := setupPGGovernanceServer(t, pool)
|
||||
ctxMgr := bizdev.Snapshot(srv.BizdevService()).Ctx
|
||||
if ctxMgr == nil {
|
||||
t.Fatal("context manager required")
|
||||
}
|
||||
|
||||
ownerToken := loginTestUser(t, handler, "zhanggong", "123456")
|
||||
ownerID := fetchAuthUserID(t, handler, ownerToken)
|
||||
otherToken := loginTestUser(t, handler, "lizhuguan", "123456")
|
||||
otherID := fetchAuthUserID(t, handler, otherToken)
|
||||
|
||||
suffix := time.Now().Format("150405.000")
|
||||
sessionID := "sess-pg-ctx-idor-" + suffix
|
||||
if _, err := ctxMgr.EnsureChatSession(ctx, sessionID, ownerID, 0, "PG IDOR 回归"); err != nil {
|
||||
t.Fatalf("ensure session: %v", err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO chat_messages (session_id, user_id, role, content, created_at)
|
||||
VALUES ($1, $2, 'user', 'pg-owner-private-body', now())
|
||||
`, sessionID, ownerID); err != nil {
|
||||
t.Fatalf("seed message: %v", err)
|
||||
}
|
||||
|
||||
// 他人视角:manager 层不返回会话正文。
|
||||
foreign, err := ctxMgr.GetUserContext(ctx, 0, otherID, sessionID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, m := range foreign.RecentChats {
|
||||
if strings.Contains(m.Content, "pg-owner-private-body") {
|
||||
t.Fatalf("foreign user must not read owner messages: %+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
// 归属者视角:正常取回。
|
||||
own, err := ctxMgr.GetUserContext(ctx, 0, ownerID, sessionID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, m := range own.RecentChats {
|
||||
if strings.Contains(m.Content, "pg-owner-private-body") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("owner should read own session messages")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +189,10 @@ func writeIntentPlanError(w http.ResponseWriter, r *http.Request, plans intentfl
|
||||
status = http.StatusNotFound
|
||||
case errors.Is(err, intentflow.ErrPlanActionForbidden):
|
||||
status = http.StatusForbidden
|
||||
case errors.Is(err, intentflow.ErrPlanModeNotConfirmable),
|
||||
errors.Is(err, intentflow.ErrPlanModeNotExecutable):
|
||||
// validated_only(shadow/dry-run)计划的硬边界:该动作对这类计划永远不可用。
|
||||
status = http.StatusForbidden
|
||||
case errors.Is(err, intentflow.ErrPlanStaleRevision),
|
||||
errors.Is(err, intentflow.ErrPlanExpired),
|
||||
errors.Is(err, intentflow.ErrPlanDecisionConflict),
|
||||
|
||||
@@ -86,6 +86,8 @@ func u5Create(t *testing.T, plans *intentflow.Plans, userID int64, projectID int
|
||||
RequestDigest: turn + "-digest",
|
||||
WritePlan: true,
|
||||
SafeSummary: "query then generate",
|
||||
// 本组用例验证 live 确认/回执语义;缺省 validated_only 不可 Confirm。
|
||||
ExecutionMode: intentflow.ModeLive,
|
||||
Steps: []intentflow.StepDraft{
|
||||
{ActionKey: "regulation.query", ActionDigest: "q1"},
|
||||
{ActionKey: "task.run", ActionDigest: "w1"},
|
||||
|
||||
@@ -56,6 +56,8 @@ func TestHandleRunProjectWorkflow_DecodesBodyWithoutContentLength(t *testing.T)
|
||||
cfg := &config.Config{
|
||||
DataDir: t.TempDir(),
|
||||
AgentBackend: "stub",
|
||||
// FIND-SEC-02: 业务行为用例走显式内存授权(无库模式项目访问需显式声明)。
|
||||
AllowInMemory: true,
|
||||
}
|
||||
svc := bizdev.MustNewService(t, cfg)
|
||||
srv := NewServer(cfg, svc)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCreateTaskRunBusyPreflightDoesNotPersistRunOrMessages(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
srv := newServerWithMemoryIAM(t, nil) // FIND-SEC-02: 业务行为用例走显式内存授权
|
||||
ctx := t.Context()
|
||||
docID := "doc_proj143_intake"
|
||||
if _, err := srv.gov().CreateOrGetDocument(
|
||||
@@ -74,7 +74,7 @@ func TestCreateTaskRunBusyPreflightDoesNotPersistRunOrMessages(t *testing.T) {
|
||||
|
||||
// 从未激活项目:任务作用域为 pre_activation,创建 run 必须 202 而非 TASK_SCOPE_UNRESOLVED 409。
|
||||
func TestCreateTaskRun_PreActivationAcceptedNot409(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
srv := newServerWithMemoryIAM(t, nil) // FIND-SEC-02: 业务行为用例走显式内存授权
|
||||
store := projectinstance.NewMemoryStore()
|
||||
_ = projectinstance.SeedProjectMeta(store, 9001, projectinstance.LifecyclePlanning, "", 1, 0)
|
||||
srv.setOverrideInstSvc(projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil))
|
||||
@@ -127,7 +127,7 @@ func TestCreateTaskRun_PreActivationAcceptedNot409(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateTaskRun_ScopeUnresolvedPersistsChat(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
srv := newServerWithMemoryIAM(t, nil) // FIND-SEC-02: 业务行为用例走显式内存授权
|
||||
token := loginTestAdmin(t, srv)
|
||||
srv.cfg.AllowInMemory = false
|
||||
srv.setOverrideInstSvc(nil)
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCreateTaskRun_SameTurnIdempotentReturnsExistingDespiteBusyLease(t *testing.T) {
|
||||
srv := newTestServer(t)
|
||||
srv := newServerWithMemoryIAM(t, nil) // FIND-SEC-02: 业务行为用例走显式内存授权
|
||||
ctx := t.Context()
|
||||
token := loginTestAdmin(t, srv)
|
||||
userID := int64(1)
|
||||
|
||||
@@ -147,9 +147,9 @@ func (e *ControlledPatchExecutor) Execute(ctx context.Context, req ControlledPat
|
||||
}
|
||||
res.Files = files
|
||||
|
||||
allowedPrefixes := e.Allowlist.AllowedCommands(res.RepoPath)
|
||||
allowedNames := e.Allowlist.AllowedCommands(res.RepoPath)
|
||||
for _, cmd := range req.Commands {
|
||||
if !commandMatchesPrefixes(cmd, allowedPrefixes) {
|
||||
if !commandAllowed(cmd, allowedNames) {
|
||||
return fail(fmt.Errorf("swboot: command not in admin allowlist: %.80s", cmd))
|
||||
}
|
||||
rec, err := e.runCommand(ctx, wtDir, cmd)
|
||||
@@ -176,10 +176,15 @@ func (e *ControlledPatchExecutor) runCommand(ctx context.Context, dir, cmdLine s
|
||||
maxOut = defaultMaxOutputBytes
|
||||
}
|
||||
rec := ExecutionCommand{Command: cmdLine}
|
||||
// FIND-SEC-03 回归:argv 化执行,不经 shell。命令行中的元字符
|
||||
//(;、&&、|、$() 等)只会成为 argv 参数,不会被求值。
|
||||
fields := strings.Fields(strings.TrimSpace(cmdLine))
|
||||
if len(fields) == 0 {
|
||||
return rec, fmt.Errorf("swboot: empty command")
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
// sh -c with a sanitized environment: no secrets, no repo .env leakage.
|
||||
c := exec.CommandContext(cctx, "sh", "-c", cmdLine)
|
||||
c := exec.CommandContext(cctx, fields[0], fields[1:]...)
|
||||
c.Dir = dir
|
||||
c.Env = []string{
|
||||
"PATH=" + os.Getenv("PATH"),
|
||||
@@ -301,18 +306,21 @@ func (e *ControlledPatchExecutor) persist(ctx context.Context, req ControlledPat
|
||||
}
|
||||
}
|
||||
|
||||
func commandMatchesPrefixes(cmd string, prefixes []string) bool {
|
||||
// commandAllowed 判定命令是否在 admin 登记的命令名白名单内。
|
||||
// FIND-SEC-03 回归:只允许首段 basename 与白名单条目「精确相等」;
|
||||
// 前缀匹配(go 放行 go-evil)与 shell 拼接均已移除。
|
||||
func commandAllowed(cmd string, allowed []string) bool {
|
||||
fields := strings.Fields(strings.TrimSpace(cmd))
|
||||
if len(fields) == 0 {
|
||||
return false
|
||||
}
|
||||
seg := filepath.Base(fields[0])
|
||||
for _, p := range prefixes {
|
||||
for _, p := range allowed {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if seg == p || strings.HasPrefix(seg, p) {
|
||||
if seg == p || seg == filepath.Base(p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,3 +160,44 @@ func TestApproveMerge_NeverExecutes(t *testing.T) {
|
||||
t.Fatalf("merge must stay behind separate authorization: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutor_CommandAllowlistIsExactNotPrefix FIND-SEC-03 回归:
|
||||
// 白名单是精确命令名匹配,前缀伪装(echo 放行 echo-evil)必须被拒。
|
||||
func TestExecutor_CommandAllowlistIsExactNotPrefix(t *testing.T) {
|
||||
if commandAllowed("echo-evil pwn", []string{"echo"}) {
|
||||
t.Fatal("prefix spoof 'echo-evil' must not match allowlist entry 'echo'")
|
||||
}
|
||||
if !commandAllowed("echo ci-ok", []string{"echo"}) {
|
||||
t.Fatal("exact command 'echo' must match")
|
||||
}
|
||||
if !commandAllowed("/usr/bin/echo hi", []string{"echo"}) {
|
||||
t.Fatal("absolute-path basename must match entry 'echo'")
|
||||
}
|
||||
if commandAllowed("", []string{"echo"}) || commandAllowed(" ", []string{"echo"}) {
|
||||
t.Fatal("empty command must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutor_NoShellEvaluation FIND-SEC-03 回归:argv 化执行后,命令行中\n// 的 shell 元字符只作为字面参数传给命令,不会被 shell 求值。
|
||||
func TestExecutor_NoShellEvaluation(t *testing.T) {
|
||||
repo := initTestRepo(t)
|
||||
marker := filepath.Join(t.TempDir(), "sh-pwn-marker")
|
||||
e := newExecutor(&fakeAllowlist{
|
||||
paths: map[string]bool{repo: true},
|
||||
commands: map[string][]string{repo: {"echo"}},
|
||||
})
|
||||
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
||||
ProjectID: 11, RepoPath: repo, BaseRef: "HEAD",
|
||||
Patches: []string{samplePatch},
|
||||
Commands: []string{"echo x && touch " + marker},
|
||||
})
|
||||
if err != nil || res.Status != "succeeded" {
|
||||
t.Fatalf("echo with literal metachar args should succeed: %+v %v", res, err)
|
||||
}
|
||||
if len(res.Commands) != 1 || !strings.Contains(res.Commands[0].Output, "x && touch") {
|
||||
t.Fatalf("output should be literal echo args: %+v", res.Commands)
|
||||
}
|
||||
if _, statErr := os.Stat(marker); statErr == nil {
|
||||
t.Fatal("shell metacharacters must not be evaluated; marker file was created")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user