diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index 5ccc9c5..535eccc 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -47,7 +47,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端, ## 当前阶段 -当前项目已完成 Phase 0~2、T-301 与审核整改 `T-604`~`T-611`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线、双适配器交互契约、`VisibleItems` 快照生命周期、双端 Gio shell 职责拆分以及 unsafe cache 安全诊断/runbook 均已关闭;下一步按 Phase 1 安全审核最终顺序落成中央目录/EOCD 预扫描边界任务,T-302 暂后置。 +当前项目已完成 Phase 0~2、T-301 与审核整改 `T-604`~`T-611`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线、双适配器交互契约、`VisibleItems` 快照生命周期、双端 Gio shell 职责拆分以及 unsafe cache 安全诊断/runbook 均已关闭;`T-612` 已落成为中央目录/EOCD 预扫描边界任务并是当前唯一待领取项,T-302 暂后置。 优先路径: @@ -55,7 +55,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端, 2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。 3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。 4. 已完成 T-604:modern/Win7 workspace 与 Gio 版本解析彻底隔离。 -5. 已完成 T-606~T-611:图标缓存资源边界、UI 线程事件接线、双 Gio 适配器交互契约、`VisibleItems` generation 生命周期、双端 `shell.go` 同 package 镜像职责拆分和 unsafe cache 诊断/人工恢复指引;下一步串行处理 Phase 1 中央目录预扫描整改,再继续 T-302/T-303 与 Phase 4-6。 +5. 已完成 T-606~T-611:图标缓存资源边界、UI 线程事件接线、双 Gio 适配器交互契约、`VisibleItems` generation 生命周期、双端 `shell.go` 同 package 镜像职责拆分和 unsafe cache 诊断/人工恢复指引;下一步执行 T-612 的 Phase 1 中央目录预扫描整改,再继续 T-302/T-303 与 Phase 4-6。 ## 领取任务规则 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 2a938ca..26f8ba4 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -42,11 +42,12 @@ #### Phase 1 交叉审核加固 -Phase 1 安全整改按 `docs/review/phase1-security-review.md` 的交叉复核定稿顺序串行落成。T-605 关闭前不恢复 T-302;后续中央目录、断电耐久和签名向量任务在前一整改完成并提交后再正式编号。 +Phase 1 安全整改按 `docs/review/phase1-security-review.md` 的交叉复核定稿顺序串行落成。T-605 与 T-612 关闭前不恢复 T-302;后续断电耐久和签名向量任务在前一整改完成并提交后再正式编号。 | ID | 任务 | 依赖 | 验收要点 | | --- | --- | --- | --- | | T-605 | 统一 Windows 安全路径校验并封堵 ZIP 逃逸 | T-102, T-201, T-202, T-604 | Catalog/ZIP/installed-app 共用逐段 Windows 安全相对路径策略;拒绝尾随空格/点与 DOS 设备名;输出路径增加 destination 包含性兜底;原生 Windows 用例证明不写出 staging | +| T-612 | 在 ZIP 打开前限制包大小与中央目录元数据 | T-605 | 已验签 Catalog size、已完成普通下载文件长度与同句柄 EOCD/ZIP64 预扫描一致;在 `zip.NewReader` 前限制原始包、中央目录与声明条目数 | ### Phase 2 · 清单与软件列表 diff --git a/docs/current-state.md b/docs/current-state.md index d947805..e91c7ec 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -13,7 +13,7 @@ ## 当前快照 - 日期:2026-07-18 -- 阶段:Phase 2 已完成(T-201~T-204);Phase 3 的 T-301 可恢复下载队列已完成;审核整改 T-604~T-611 已完成,T-302 继续暂后置 +- 阶段:Phase 2 已完成(T-201~T-204);Phase 3 的 T-301 可恢复下载队列已完成;审核整改 T-604~T-611 已完成,T-612 已落成为唯一 TODO,T-302 继续暂后置 - 技术栈:根 Go 1.25 workspace 只纳入 core/app-modern,`app-win7/go.work` 独立纳入 core/app-win7;版本闸门证明 modern Gio v0.10.1 与 win7 Gio v0.6.0 不交叉解析 - 生产代码:core 已有 Catalog/本地状态/存储、共享 Windows 安全相对路径策略、安全 ZIP 解压/回滚原型、发布稳定只读 generation 的无 IO 软件列表模型、按 key in-flight + 流式有界读取 + 32 MiB/256-key LRU 的可信图标缓存、图标 Load/Decode 事件发布用例、有界 application event relay,以及默认并发 2 的持久可恢复下载队列;modern/win7 主循环已接 relay/Invalidate,AppShell 已实现搜索/分类/视图、惰性列表、详情右栏、完整图标失败 identity 生命周期与仅 `unsafe_cache` 可见的安全 locator/人工恢复提示,并按 root/header/catalog/detail/style 同 package 镜像职责拆文件 - 测试:core 覆盖 Catalog、列表快照 generation/零复制、SemVer/12 状态、本地安装记录、Windows dot-space/设备名/Unicode 折叠路径攻击、ZIP destination 包含性、图标并发/取消/读取边界/LRU、真实目录/symlink fail-closed 与 cache→`unsafe_cache` event、relay 背压与关闭、下载并发/暂停/取消/重试/Range/断连/恢复/事件失败与文件身份替换;两个 app 覆盖 Editor/视图/分类/行/恢复/关闭接线、500 项 viewport、AppID 控件与分类控件生命周期、详情上下文、空状态语义、UI drain 前后、图标失败身份生命周期与 `unsafe_cache` 详情语义;安装恢复矩阵保持通过 @@ -21,14 +21,14 @@ - 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、执行完整 Phase 0 闸门、打印双目标构建命令) - 标准验证路径:`bash scripts/verify_phase0.sh` / `./scripts/verify_phase0.ps1` - 版本管理:git 已初始化,main 分支,远端 origin 为 Gitea `opc/soft_quay`;harness 文档已提交 -- 当前 blocker:无;T-611 已闭合 unsafe cache 诊断且自动 quarantine 仍需独立威胁模型。下一步按 `docs/review/phase1-security-review.md` 最终处理顺序第 2 项落成后续任务(预计 T-612):在 `zip.OpenReader` 前增加包大小与中央目录/EOCD 预扫描边界;T-302 继续后置到该阻断项关闭 +- 当前 blocker:无;T-611 已闭合 unsafe cache 诊断且自动 quarantine 仍需独立威胁模型。`T-612` 已落成,将在 ZIP reader 构造前增加 Catalog size、完成文件长度与中央目录/EOCD(含 ZIP64)预扫描边界;T-302 继续后置到该阻断项关闭 ## 当前目录要点 | 路径 | 状态 | 说明 | | --- | --- | --- | | `docs/` | 已有 | harness coding 文档集(本次初始化完成) | -| `docs/tasks/` | 已有 | Phase 0~2、T-301 与 T-604~T-611 已完成;Phase 1 后续审核整改尚未编号,T-302 暂后置 | +| `docs/tasks/` | 已有 | Phase 0~2、T-301 与 T-604~T-611 已完成;T-612 是当前唯一 TODO 的 Phase 1 中央目录/EOCD 预扫描整改,T-302 暂后置 | | `scripts/` | 已有 | harness 治理、core 边界、Go 版本检查与 Phase 0 双平台验证入口 | | `core/` | 已建 | Go 1.20 兼容;已有正式 Catalog、本地状态/存储、共享 Windows safepath、列表模型、有界并发图标缓存、图标事件/relay、可恢复下载队列与 Phase 1 安装安全原型 | | `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1;Modern AppShell 已接入虚拟列表、详情、图标事件 drain/过期拒绝和内存 ImageOp,并拆为五类 shell 职责文件 | @@ -42,7 +42,7 @@ - 已完成:Phase 0 的 `T-001`~`T-004`;Phase 1 的 `T-101`、`T-102`、`T-103`;Phase 2 的 `T-201`~`T-204`;Phase 3 的 `T-301`;审核整改 `T-604`~`T-611`。 - 正在进行:无。 -- 下一个可领取任务:暂无已落成 TODO;下一步按 Phase 1 最终处理顺序第 2 项落成中央目录/EOCD 预扫描边界任务(预计 `T-612`)。 +- 下一个可领取任务:`T-612` — 在 ZIP reader 构造前增加已验签 Catalog size、完成文件长度和中央目录/EOCD(含 ZIP64)预扫描边界。 ## 当前可运行内容 diff --git a/docs/tasks/T-612.md b/docs/tasks/T-612.md new file mode 100644 index 0000000..26288d1 --- /dev/null +++ b/docs/tasks/T-612.md @@ -0,0 +1,71 @@ +--- +id: T-612 +title: 在 ZIP 打开前限制包大小与中央目录元数据 +phase: 1 +deps: [T-605] +status: TODO +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。