Files
soft_quay/docs/tasks/T-612.md

75 lines
9.4 KiB
Markdown

---
id: T-612
title: 在 ZIP 打开前限制包大小与中央目录元数据
phase: 1
deps: [T-605]
status: DONE
created: 2026-07-18
issue: null
context_ref: 320b83d929fde7d51fc5bb3e9fbdaac7beb5101e
claim_branch: null
work_branch: agent/codex/T-612
write_paths:
- docs/tasks/T-612.md
- core/installer/
- docs/api.md
- docs/04-architecture.md
- docs/05-coding-rules.md
- docs/review/phase1-security-review.md
- docs/00-ai-start-here.md
- docs/06-tasks.md
- docs/current-state.md
---
## 问题 / 背景
`Extractor.ExtractFile` 当前直接调用 `zip.OpenReader(zipPath)`,而现有 `preflight` 对 `len(archive.File)`、展开大小和压缩比的检查发生在标准库已经读取并解析 ZIP 中央目录之后。攻击者若能让已验证的发布包携带巨大或伪造的中央目录元数据,仍可在协议级检查开始前触发不受本模块限制的元数据读取/分配。根据 `docs/review/phase1-security-review.md` 定稿,这属于“签名内容管线的纵深防御缺口”,不是未签名网络输入可直接利用的路径,但仍阻断 T-302 正式安装整合。
T-301 已把 known-total 下载限定为精确长度,并在 final `.download` 发布前核对普通文件身份与实际长度;Catalog `packages[arch].size` 也是签名内容。当前 `Extractor` 只有测试调用方,T-302 尚未把 Catalog、下载完成文件、SHA-256 与解压器编排为生产链路。因此本任务应先把三者对账所需的 installer API 和 fail-closed 边界落实,不得把测试契约误写成生产安装已接线。
## 方案
1. 扩展 `installer.Limits`,增加 ZIP 原始包大小和中央目录字节数两个显式硬上限;默认原始包上限与 T-301 `DefaultMaxUnknownBytes` 的 4 GiB 上限一致,中央目录采用独立、明显更小的暂定安全上限。`NewExtractor` 必须拒绝零值、负值、溢出或彼此矛盾的配置;现有条目数、展开体积和压缩比硬上限保留。
2. 把 `Extractor.ExtractFile` 的调用契约改为显式接收已验签 Catalog 的 `expectedPackageSize`。在任何 ZIP 解析前,它必须:
- 打开并只使用同一个普通文件句柄,以该句柄的真实长度同时核对 `expectedPackageSize`、`MaxArchiveBytes` 与后续 ZIP reader 的 size;不允许先扫描路径再按同一路径重新打开,避免本地替换窗口。
- 拒绝未知、非正或不匹配的预期大小,以及符号链接/非普通包文件;返回可由 `errors.Is` 识别的稳定 installer 错误,且不创建 staging。
- 从文件尾部以固定上界搜索 EOCD(最多 ZIP 规范允许的 65,535 字节 comment 加 EOCD 固定字段),不把整个文件或整个中央目录读入内存。校验单磁盘语义、EOCD 位置、中央目录 offset/size/entries 的无溢出区间关系和中央目录必须在 EOCD 前结束。
- 支持并严格校验 ZIP64 locator/ZIP64 EOCD 的对应字段;不因合法 ZIP64 仅因 32 位 EOCD 哨兵值而误拒绝。无法完整证明一致性的 EOCD、跨盘 archive、截断、伪造 offset 或不支持的结构一律按无效 archive 拒绝。
- 在构造 `zip.Reader` 前以 EOCD 声明条目数和中央目录字节数分别执行 `MaxEntries` 与 `MaxCentralDirectoryBytes` 检查;再使用已经扫描的同一打开句柄和受核对的实际长度创建 `zip.NewReader`。现有完整 `preflight` 必须保留,作为对标准库解析结果、ZIP entry 语义和展开数据的第二道检查。
3. 冻结错误及 API 文档:大小不一致、原始包过大、中央目录过大、声明条目过多和格式错误要有可测试的 stable sentinel/错误链;调用方只能将来自已验签 Catalog 的 exact `size` 传入。T-302 必须重新取得 Catalog 身份并把已完成 `.download` 的实际长度、Catalog `size` 与本 API 的预扫描结果串成同一条安装链。
4. 添加攻击回归,至少覆盖:正常经典 ZIP;真实包大小与 expected size 不一致;原始文件超限;经典 EOCD 伪造 entries 或 central-directory size;越界/截断/跨盘 EOCD;ZIP64 成功路径及不一致 locator/record;以及被预扫描拒绝时 destination 不存在。测试要断言错误发生在 `zip.NewReader`/旧 `preflight` 之前能够确定的 sentinel 上,而非退化为“解析后才失败”。保留并回归现有路径逃逸、压缩比、展开量、CRC 和 staging 清理测试。
5. 同步 API、架构和编码规则:把“先对账已验证 Catalog size、已完成普通下载文件长度,再作有界 EOCD/中央目录预扫描,最后才构造 ZIP reader”列为安装安全顺序;明确限额仍是暂定值,T-302 按真实包分布复核。Phase 1 审核记录和项目当前状态必须如实标记该阻断项已关闭或仍待 T-302 接线。
## 验收要点
- `Limits` 同时包含并验证 `MaxArchiveBytes`、`MaxCentralDirectoryBytes`、现有 `MaxEntries`、展开大小与压缩比;默认原始包上限与 T-301 4 GiB unknown-total 限制一致,中央目录限制独立且不允许绕过。
- `ExtractFile` 在任何 `zip.Reader` 构造前,以同一普通文件句柄验证 expected Catalog size 与真实文件长度相等、且不超原始包上限;不匹配/未知/非普通输入 fail closed,不创建 destination。
- EOCD 预扫描只读取固定上界尾部和 ZIP64 所需固定记录,不分配与 archive 长度或声明目录大小成比例的内存;经典与 ZIP64 的 entries、offset、size、磁盘号、边界和溢出均被验证。
- EOCD 声明 entries 超 `MaxEntries` 或中央目录超 `MaxCentralDirectoryBytes` 时,返回对应稳定错误且不进入标准库中央目录解析;损坏、截断、跨盘或不一致的 ZIP64 结构返回 `ErrInvalidArchive` 错误链。
- 有效的经典 ZIP 和 ZIP64 元数据路径仍能完成现有安全提取;现有路径、entrypoint、类型、重复名、CRC、展开体积和压缩比防线不回退。
- `docs/api.md`、`04-architecture.md`、`05-coding-rules.md` 和审核记录准确写明 size 三方对账、同句柄预扫描、ZIP64 策略、临时限额与“生产 T-302 接线尚未完成”的边界。
- Go 1.20.14 + `GOWORK=off` 下 `go vet ./installer`、`go test -count=10 ./installer` 通过;完整 `./scripts/verify_phase0.ps1`、`python scripts/validate_agent_context.py`、`python scripts/validate_harness_governance.py` 与提交前差异检查通过。
## 边界(不改什么)
- 不实现 T-302 的下载→SHA-256→Catalog 身份→app.json→安装编排,不把 `core/downloader` 或 `cmd` 误改成已有生产调用方;本任务只提供 T-302 必须使用的 Extractor 契约和测试证据。
- 不改 Catalog 签名、SHA-256、下载队列协议、manifest/schema 或 package 格式;不从 ZIP 内部字段放宽 Catalog 的预期大小。
- 不移除既有中央目录语义 `preflight`,不降低 Windows 安全路径、entrypoint、压缩比、展开量、CRC、全新 staging 或失败清理防线。
- 不在本任务实现 payload 文件 `Sync()`、目录/journal 的断电耐久顺序或 Windows VM 故障注入;这是审核顺序下一项的独立任务。
- 不引入第三方 ZIP 解析库,不读取完整 archive/中央目录到缓冲区,不改 Go/Gio 版本、workspace、UI、用户的 `soft_quay.code-workspace` 或无关任务。
## 协作约束
- 按仓库当前规则由单 Agent 串行执行,不启动子 Agent。
- 本任务只允许修改 frontmatter 中的 `write_paths`;若需要生产安装编排、下载队列协议变化、SHA-256 再校验或断电耐久实现,必须停止并另立任务,不得在 T-612 内扩边。
- T-612 完成、完整验证并提交前,不落成或领取下一项 Phase 1 耐久性整改或 T-302。
## 执行记录
- 2026-07-18:根据 `docs/review/phase1-security-review.md` 最终处理顺序第 2 项落成任务;现有全局最大任务为 T-611,因此取 T-612,依赖已完成的 T-605。
- 2026-07-18:代码图确认 `ExtractFile` 直接调用 `zip.OpenReader`,随后才在 `preflight` 用 `len(archive.File)` 检查条目数;其调用方目前均为 installer 测试。T-301 已对 known-total 下载以已验签 Catalog size 严格限长并发布普通 final 文件,但尚无生产安装编排。因此本任务通过显式 `expectedPackageSize` 和同句柄预扫描建立正确接口,不宣称已完成 T-302 接线。
- 2026-07-18:默认 `MaxArchiveBytes` 将与 downloader `DefaultMaxUnknownBytes` 的 4 GiB 保持一致;中央目录单独限额及 ZIP64 兼容性必须以测试和文档冻结,真实包采样后的阈值复核仍留给 T-302。
- 2026-07-18:实现 `MaxArchiveBytes=4 GiB`、`MaxCentralDirectoryBytes=64 MiB` 和对应配置校验;`ExtractFile` 现在强制接收 expected Catalog size,打开同一普通文件后先核对大小并执行有界 EOCD/ZIP64 扫描,再用该句柄构造 `zip.NewReader`。声明 entries/central size 在标准库解析前受限,跨盘、截断、越界与不一致 ZIP64 结构 fail closed,既有完整 `preflight` 保留。
- 2026-07-18:installer 回归新增 size 不一致、原始包超限、经典 EOCD 条目/中央目录伪造、跨盘、越界、截断、ZIP64 成功/损坏及非普通输入矩阵,全部确认拒绝时不创建 staging;既有路径、CRC、展开量、压缩比与 destination 清理回归继续通过。
- 2026-07-18:验证通过:`GOWORK=off go vet ./installer`、`GOWORK=off go test -count=10 ./installer`、`./scripts/verify_phase0.ps1`、`python scripts/validate_agent_context.py`、`python scripts/validate_harness_governance.py` 与提交前 `git diff --check`;完整 Phase 0 闸门同时复跑 core 全包、modern/Win7 UI 测试与双目标构建。