Repository navigation
Validate retained files before static-site rollback - #6213
huangruiteng merged 1 commit into
Conversation
Treat retained revision metadata as untrusted before copying files. Reject paths outside the revision, symlinks, binding mismatches, and content drift before staging a rollback. Signed-off-by: Duang777 <duangjl007@gmail.com>
|
@loopx-agent review |
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent | gpt-6.1-sol | OpenAI | runtime_reported | reasoning_effort=xhigh
Exact reviewed head: 6213@9081da5ee4f294036b082a1c1d5fb9a0ba47ce79; immutable base: dbbca15b5e1848e89a605676c17a28ff47f662d4. 两文件完整 diff、既有生产调用者和保留版本写入器已读;最新主干的相关 owner 没有改变。未发现阻塞项。
动机
回滚已保留静态网站版本的 CLI 操作者。
以前保留版本的文件清单被直接用于复制;路径穿越可覆盖输出目录旁的文件,内容损坏也会被当成成功回滚。现在先验证所选版本,再准备替换;失败保留当前网站,修复后可继续回滚。
独立真实 CLI 复现基线越界写入;head 拒绝且输出、日志和目录外文件不变。合法回滚、预览、新版本延续及修复后恢复成立。
本轮覆盖本地保留版本校验及回滚,不部署外部网站,不认证可由本地写者伪造的摘要或验证声明。
规格依据为 no_spec:先检索上述不可变 base 的文档及现有讨论,没有找到专门的已接受静态网站回滚规格,也没有关联 issue。因此不编造 RFC 验收编号。当前任务来自可独立复现的错误;基线 package_static_site 的写入约定,以及 既有 package/rollback/HTTP smoke 是普通合法调用和恢复行为的参照。
改动思路
先核验目标版本中的既有数据,再复用原来的复制、暂存替换和回执 owner。仅做路径检查仍会放过错误版本绑定、损坏字节或缺失入口;重新扫描整个版本目录又不能代表历史声明的准确文件集合。这次局部验证比新增持久化框架或迁移协议更合适。
Existing packaging produces all fields now checked. Validate the selected persisted input at rollback, reuse copy/replace/receipt owners, and keep the staged replacement after complete validation. A general persistence/authority framework is unnecessary.
当前 PR 的完整边界是:所选保留版本通过校验后才准备替换,并经真实 CLI 验证拒绝、继续回滚和读回;没有新增发布能力。
Python 位于既有本地文件 IO 边界,不新增通用控制面决策源;不需要为这项修复做无关 TS 迁移。新读取器复用 _semantic_digest、_validation_receipt 和保留路径集合,未引入第二份状态或权限规则。
具体改动
关键代码讲解
_validate_retained_revision_files(loopx/presentation/static_site.py:254):接收历史文件清单;拒绝绝对/父级路径、重复和保留路径、符号链接、缺失文件以及错误摘要/大小。布尔值不会被误作整数大小。读取真实字节验证后返回规范化清单,整个过程没有写入。rollback_static_site(同文件:555):CLI 的 rollback 分支进入这里。先核验目标的 site/revision 绑定、语义摘要、声明入口和原有 passed 验证回执,再组装原格式 manifest/receipt。校验发生在_replace_output之前,错误走既有StaticSiteContractError和 CLI exit 2。_replace_output(同文件:355):沿用已有暂存构建、替换及替换失败恢复。两处 fresh-package 复制入口继续使用_collect_site_files生成的清单,历史 rollback 入口则使用本次验证结果;不把相似复制过程错误合成另一套 authority。
生产文件增加 115 行、删除 6 行;测试增加 62 行,覆盖路径穿越和保留内容损坏。新增 SHA256 正则属于该文件的局部输入校验,已有 schema/version、reserved 集合及验证值均复用,没有新共享词汇。
独立基线/head 真实 main() + 文件系统执行 23 组场景:普通恢复、预览、缺失版本;父级/绝对路径;重复/保留文件;文件、父目录和版本目录符号链接;错误 site/revision;无效摘要、布尔/负数大小;内容/语义摘要损坏;缺失入口和验证回执;错误条目类型和空/非数组清单。Head 的全部预期成立;拒绝时按完整目录树、当前 manifest 和事件日志验证没有写入。基线的 traversal 调用 exit 0 并覆盖目录外测试文件,head exit 2 并保留原内容,证明检查可以识别原缺陷。
继续执行三版本流程:生成 one/two/three;保留 one 的错误 metadata,仍能恢复未受影响的 two;随后恢复新版本 three;修复 one 后再次恢复并分别读取 index.html/manifest。另一个内容损坏案例在 head 拒绝后修复原字节,成功回滚,再继续到原 active 版本。不是只得到一张“拒绝回执”。
对主干的风险
更严格的历史输入拒绝是公开说明的默认变化;基线生产写入器已包含所有新增必检字段,普通既有版本无需迁移。只核验调用者选定的目标,不要求其他保留版本一起重新通过认证。合法预览保持无写入,明确 execute 后的 manifest/receipt/event 结构沿用原 owner。
本地验证:head 11 passed,base 9 passed;两个版本的真实 loopback HTTP smoke 都通过。原生 premerge 5 项 direct、2 项 selected 全通过,包含完整语义检查,manual holds 为 0;两侧 Ruff 均通过。开发期 diff 语义 advisory 在 full 检查之前执行,支持范围候选为 0,不以空结果证明语义等价。未查询、轮询或等待 GitHub CI。
最初测试到达 100% 后清理历史 pytest 临时目录过慢,已中断本轮自己的清理进程,并用独立 --basetemp 重跑相同用例取得完整 exit 0 结果;没有把中断运行算作通过。最初基线反例脚本错误地要求已发生错误回滚的 base 还能按 head 的恢复次序成功,已去掉该假设,保留基线错误行为及 head 恢复读回。
剩余边界:本地可写者仍可共同伪造内容、摘要或 passed 声明;本次结构/完整性验证不是签名认证。没有证明恶意并发写者在检查后替换文件的抗竞态能力,也没有远程部署资格。这些未被本 PR 承诺,不能把它们写成已经通过的验证。没有新的 actor lifecycle、自动执行授权或 default-off capability,也没有第一屏呈现改动。
我的整体评价
APPROVE。这个完整切片通过实际调用阻止越界/损坏恢复,同时保留普通历史版本、预览及后续恢复路径;不以行数、作者自测或测试数量替代结果判断。
未来相关重构检查已执行:保留目录校验属于当前 rollback owner,复用既有摘要、验证及替换函数足够;未发现必须在此 PR 引入的新框架或另一套共享规则。当前比例适当,保留既有持久化格式。批准不授予部署或合并权限。
English verdict: APPROVE — 6213@9081da5ee4f294036b082a1c1d5fb9a0ba47ce79. No blocking finding. The retained-target validator prevents the reproduced baseline path escape and corruption before staging, while actual CLI preview, three-version continuation, unrelated-revision isolation and repair/retry readback remain usable. 11 head/9 base tests, 23 paired cases, both real HTTP smokes, Ruff and all 5 direct/2 selected premerge checks pass. Local hashes/validation receipts are not signed authenticity or a hostile-writer concurrency guarantee; no remote deployment or GitHub CI was consulted.
Summary
Root cause
rollback_static_sitepassed persistedREVISION_FILEpaths directly to_copy_declared_files. A modifiedfiles[*].pathcould escape both the retained revision and the staging directory. In a concrete reproduction,../../escaped.txtoverwrote an existing file besideoutput_dir. Rollback also did not compare retained file bytes with their recorded size and SHA-256 digest.Validation
DID NOT RAISE; the path case overwrote the file outsideoutput_dir.tests/test_static_site_presentation.pypasses, 11 tests total.revision file path must stay inside the retained revision, and the outside file remains unchanged.loopx checkreports a clean public boundary for both changed files.loopx canary premerge --from-git-diff --git-diff-base upstream/mainpasses both selected canaries.upstream/main; this change adds none.Sibling sweep
_copy_declared_fileshas three callers. Both package callers use the fresh list from_collect_site_files; only rollback consumed persisted metadata without validation. No sibling change is required.uv.lockis unchanged.