2026-07-16 23:36:39 +08:00
---
id : T-605
title : 统一 Windows 安全路径校验并封堵 ZIP 逃逸
phase : 1
deps : [ T-102, T-201, T-202, T-604]
2026-07-16 23:56:19 +08:00
status : DONE
2026-07-16 23:36:39 +08:00
created : 2026-07-16
issue : null
2026-07-16 23:56:19 +08:00
context_ref : f7a803d944d3ef1c782aa974d5114f51d3e00b17
2026-07-16 23:36:39 +08:00
claim_branch : null
2026-07-16 23:56:19 +08:00
work_branch : agent/codex/T-605
2026-07-16 23:36:39 +08:00
write_paths :
- docs/tasks/T-605.md
- core/internal/safepath/
- core/catalog/parser.go
- core/catalog/parser_test.go
- core/installer/extractor.go
- core/installer/extractor_test.go
- core/installer/extractor_windows_test.go
- core/storage/installed_app.go
- core/storage/installed_app_test.go
- schemas/manifest.schema.json
- schemas/app.schema.json
- schemas/installed-app.schema.json
- testdata/zip/README.md
- docs/api.md
- docs/04-architecture.md
- docs/05-coding-rules.md
2026-07-16 23:56:19 +08:00
- docs/00-ai-start-here.md
2026-07-16 23:36:39 +08:00
- docs/current-state.md
---
## 问题 / 背景
Phase 1 安全交叉审核确认,T-102 的 `normalizeArchivePath` 与 `normalizeEntrypoint` 、T-201 的 Catalog `entry_exe` 校验、T-202 的 installed-app 文件路径校验都以 POSIX `path.Clean` 为核心,没有覆盖 Windows 普通 DOS 路径的名称归一化规则。
Windows 会忽略路径段末尾的 ASCII 空格和句点,并保留 `CON` 、`PRN` 、`AUX` 、`NUL` 、`COM1` ~ `COM9` 、`LPT1` ~ `LPT9` 等设备名及部分扩展/变体。因此 `payload/.. /escape.exe` 、`payload/file.` 、`payload/NUL` 等名称能通过当前预检,但进入 `filepath.Join` 、`MkdirAll` 、`OpenFile` 后可能产生父目录语义、路径别名或设备访问。
Extractor 的正式调用约束是 Catalog 签名与 ZIP SHA-256 已验证后才解压,所以这不是任意网络字节可直接利用的开放 RCE;它仍是签名内容管线中必须关闭的纵深防御缺口。关闭前 T-302 不得把现有 Extractor 作为“无路径穿越”的正式安装基础。
## 方案
1. 在 `core/internal/safepath` 建立 Go 1.20 兼容、无 Windows API 依赖的共享 Windows 安全相对路径策略:
- 输入统一使用 UTF-8 与 `/` 分隔。
- 拒绝空路径、绝对/UNC/盘符/ADS、反斜杠、NUL、控制字符和 Windows 禁止字符。
2026-07-16 23:56:19 +08:00
- 逐段拒绝空段、`.` 、`..` 、首尾 ASCII 空格和尾随句点。
2026-07-16 23:36:39 +08:00
- 大小写不敏感地拒绝 DOS 设备名及其在 Win7~Win11 需要兼容的扩展/变体。
- 分别支持“必须指向文件”和“工作目录可为 `.` ”两类调用语义,避免各模块自行放宽。
- 提供统一的 Windows 大小写折叠冲突键,替换 catalog/installer/storage 中分散的路径规则。
2. Catalog `entry_exe` 、Extractor ZIP entry/entrypoint、installed-app `files[].path` 全部接入共享策略,删除重复的私有 `path.Clean` 校验。后续 T-302 读取 app.json/files.json 时必须复用该策略,不得再造第四套规则。
3. Extractor 在创建 staging 前完成全部输出路径规划,并在安全路径校验之后对 `filepath.Join(destination, outputPath)` 做 destination 包含性兜底:
- 使用绝对路径与 `filepath.Rel` /等价方法拒绝绝对结果、`..` 或 destination 外目标。
- 该检查是纵深防御,不能替代逐段 Windows 名称校验。
- entrypoint 返回路径必须通过相同包含性检查。
4. 在预检阶段拒绝 Windows 归一化后会碰撞的路径,不把冲突拖到 `O_EXCL` 写入阶段才失败。
5. 扩展通用测试与 Windows 原生文件系统测试:
- `payload/.. /escape.exe` 、尾随点/空格、设备名、设备名带扩展、大小写/别名冲突。
- 拒绝发生在 staging 创建前,且 destination 外无任何新文件。
- 合法中文、嵌套目录、普通 `.exe` 和工作目录 `.` 保持可用。
6. 同步 `api.md` 、架构、安全编码规则、三个相关 Schema 与 ZIP 测试说明,明确 JSON Schema 只提供基础形状约束,运行时共享校验器才是 Windows 路径安全权威。
## 验收要点
- `core/internal/safepath` 表驱动测试覆盖 Win7~Win11 共同禁止的尾随空格/点、DOS 设备名及变体、控制字符、Windows 禁止字符、绝对/UNC/盘符/ADS、`.` /`..` 和空段。
- Catalog `entry_exe` 、ZIP entrypoint/输出路径、installed-app `files[].path` 对同一攻击路径给出一致拒绝结果;仓库不再保留三套独立的安全相对路径实现。
- ZIP `payload/.. /escape.exe` 在创建 staging 前被拒绝;测试明确断言 destination 外的 `escape.exe` 不存在。
- `payload/file` 与 `payload/file.` 、`payload/name` 与 `payload/name ` 、设备名大小写/扩展变体不会被视为不同安全输出。
- destination 包含性兜底对文件、目录和最终 entrypoint 都生效,且不能通过混合分隔符或路径清理结果绕过。
- 合法 UTF-8/中文文件名、嵌套路径和 Catalog/installed-app 既有合法样例继续通过。
- Windows 专属测试在当前可用 Windows 环境实际执行;测试代码须可在 Win7、Win10、Win11 复用。若本任务环境不能覆盖三套系统,执行记录必须写明已测系统和待 T-601 补齐的 VM/真机矩阵。
2026-07-16 23:56:19 +08:00
- `cd core && GOWORK=off GOTOOLCHAIN=go1.20.14 go vet ./... && GOWORK=off GOTOOLCHAIN=go1.20.14 go test -count=1 ./...` 通过。
2026-07-16 23:36:39 +08:00
- `./scripts/verify_phase0.ps1` 与 `bash scripts/verify_phase0.sh` 全绿,modern/Win7 双目标继续构建。
- `python scripts/validate_agent_context.py` 、`python scripts/validate_harness_governance.py` 通过。
## 边界(不改什么)
- 不处理 `zip.OpenReader` 前的 EOCD/中央目录预扫描与包大小一致性;按审核顺序另立后续任务。
- 不实现 payload 文件 `Sync` 、目录 rename/journal 的断电耐久顺序或 Windows `FlushFileBuffers` ;按审核顺序另立后续任务并在 T-302/T-601 验证。
- 不修改 Catalog canonicalization、Base64 或跨实现签名测试向量。
- 不实现 app.json/files.json 完整解析、身份比对、SHA-256/package 签名、安装编排或健康检查(T-302/T-402/T-403)。
- 不放宽现有文件数、展开体积或压缩比硬上限。
- 不引入第三方路径库,不使用 Go 1.21+ API,不修改 Gio/Go 版本。
- 不修改、提交或删除用户的 `soft_quay.code-workspace` 。
## 协作约束
- 按仓库当前规则由单 Agent 串行执行,不启动子 Agent。
- 本任务只允许修改 frontmatter 中的 `write_paths` ;发现必须改公共协议字段或扩大到中央目录/耐久性实现时先停止并记录,不得把后续整改夹带进 T-605。
- T-605 完成、完整验证并提交前,不落成或领取下一个 Phase 1 安全整改任务,不恢复 T-302。
## 执行记录
- 2026-07-16:根据 `docs/review/phase1-security-review.md` 交叉复核定稿的最高优先级整改落成任务;现有全局最大任务为 T-604,因此取 T-605。
- 2026-07-16:任务风险按定稿校准为“签名内容管线的纵深防御缺口”,同时保留阻断 T-302 的结论;本任务只收口 Windows 路径规则与 destination 纵深检查,中央目录、断电耐久和签名向量继续串行拆分。
2026-07-16 23:56:19 +08:00
- 2026-07-16:在 `agent/codex/T-605` 分支领取任务,基线为 `f7a803d944d3ef1c782aa974d5114f51d3e00b17` ;保持单 Agent 串行执行。
- 2026-07-16:新增 `core/internal/safepath` ,统一拒绝非规范/绝对路径、反斜杠/ADS、控制字符、Windows 禁止字符、首尾 ASCII 空格、尾随句点及 DOS 设备名(含扩展、上标数字和兼容变体);Unicode simple-fold 冲突键同时替换 installer/storage 中分散的 `strings.ToLower` 。
- 2026-07-16:Catalog `entry_exe` 、ZIP entry/entrypoint、installed-app `files[].path` 全部接入共享规则;Extractor 在创建 staging 前规划所有绝对输出路径,用 destination 相对包含性检查覆盖文件、目录与最终 entrypoint。攻击矩阵新增 `payload/.. /escape.exe` 、dot-space、设备名、禁止字符和 Unicode 折叠冲突,并断言 staging 外无文件。
- 2026-07-16:三个相关 Schema 加强可表达的路径形状约束并声明运行时校验器为 Windows 设备名/控制字符的权威;`api.md` 、架构、编码规则和 ZIP testdata 说明同步。Schema JSON/正则 smoke 对合法中文与 `../` 、嵌套 `..` 、尾随点、首部空格、双斜杠、`?` 拒绝均通过。
- 2026-07-16:Windows 原生测试 `TestExtractorRejectsWindowsNormalizedEscapeOnNativeFilesystem` 在 Windows 10 专业版 10.0.19045 amd64 通过;测试代码带 `windows` build tag,可直接用于 Win7/Win11。当前环境没有 Win7/Win11 VM,两者实际矩阵按验收边界留 T-601 补齐。
- 2026-07-16:验证通过:Go 1.20.14 + `GOWORK=off` 执行 `go vet ./...` 、`go test -count=1 ./...` 与 Windows 定向测试;`./scripts/verify_phase0.ps1` ;`bash scripts/verify_phase0.sh` ;modern Go 1.25.0 与 Win7 Go 1.20.14 双目标测试/构建;`python scripts/validate_agent_context.py` ;`python scripts/validate_harness_governance.py` 。