Python E08:如何审计依赖与不可信输入
一个 tasklab wheel 在干净环境里安装成功,依然无法回答两个问题:下载的到底是不是当时检验的制品?网络请求中自称“任务类型”的字符串会不会被当成可执行模块?此前 26 的安装验收解决的是构建与运行可重放;本篇用现成 wheel 和 JSON 行入口审计供应与输入边界,不引入任意代码插件。
主线 wheel tasklab-0.1.0-py3-none-any.whl 的当前路径、SHA-256、成员清单和源码文件哈希记录在 examples/python-language/evidence/completion/acceptance/wheel.json;元数据要求 Python >=3.12、没有 Requires-Dist。E05 的 Starlette/HTTPX/Uvicorn 和 E06 的 NumPy 均是可移除实验环境中的额外依赖,不能从主线 pyproject.toml 推出这些工具已被主线用户安装,也不能把 pip freeze 当作经过哈希校验的跨平台锁。
先验证制品,后讨论信任
experiments/E08_audit.py 在导入位置明确指向 venv site-packages 的情况下,对原 wheel 实际计算 SHA-256,随后读取 wheel 内 METADATA 检查名称、版本、Python 下限与空运行依赖。脚本不是根据文件名猜测内容,也没有用“本机能 import”替代制品来源。当前审计还逐项限制 wheel 成员路径:拒绝重复成员、绝对路径、反斜杠、./.. 组件和非 tasklab/、tasklab-0.1.0.dist-info/ 前缀,并要求 RECORD 覆盖全部成员且每个带哈希条目的大小和 digest 匹配。可选环境的 hash lock 来自 uv pip compile 解析的 PyPI 包与哈希,optional-freeze.stdout 只是已安装版本清单;两者不能混同。E05 冻结了 Starlette 0.46.2、Uvicorn 0.34.2、HTTPX 0.28.1 以及传递依赖,E06 冻结了 NumPy 1.26.4。已知哈希只能证明当前字节与事先保存的基准一致,不能证明基准最初可信、没有被污染,亦不能自动证明供应链无漏洞。PyPI 发布、签名认证或漏洞扫描均为 NOT_RUN。
拒绝入口:不把 JSON 变成执行指令
回环 JSON 行服务仍只识别四种 op:submit/run/query/cancel。实验逐项注入未知操作 import、不存在的任务类型 shell、多出的 file: "../secret" 字段、重复 JSON 键、非法 UTF-8 和超过 2048 字节的帧;每次响应 ok:false,并断言任务 ID 集合没有增加。随后同一服务合法提交、查询、执行 "a b a" 得到 {"a":2,"b":1},已完成任务的取消不被接受。反序列化的另一条入口 decode_task 对 kind: "shell" 和多出的字段也抛 TaskFormatError。这些是实际输入拒绝,不是“JSON 比 pickle 安全”这种对所有数据都成立的承诺;Python 官方文档明确警告不要对不可信数据调用 pickle.loads,本实验从未执行该调用。
路径边界要分清操作员与网络请求。主线 CLI 的 --file 接收操作者明确指定的本地路径,默认不是隔离的远程上传接口,不能说它已阻止目录越界。可选的 read_fixture(root, requested) 展示另一个受限入口:先对根目录和目标做 Path.resolve(strict=True)、要求结果位于根目录且是普通文件,再以最多 1025 字节读取,交给现有 submit_bytes 的 UTF-8 与 1024 字节业务检查。本机 ../secret.txt 及指向根外的符号链接 escape.txt 均被拒,原任务集合未变;合法根内文件提交与运行成功。该写法仍有“检查之后、打开之前被换掉符号链接”的 TOCTOU 风险,不是针对攻击者可并发修改文件系统的安全沙箱。若要开放上传目录,必须再设计基于目录描述符的原子打开、权限和配额,不能把这个示例直接挂在公网入口。
验收和练习
实验只从 /tmp 的已安装 wheel 环境运行,保留命令、版本、退出码、原始 JSON 和非法路径反例;examples/python-language/evidence/completion/acceptance/installed-E08.stdout 记录模块路径、wheel 元数据、RECORD_valid=true、非法网络帧和路径反例。当前 wheel 的 SHA 和成员级哈希以 acceptance/wheel.json 为准,错误 SHA 的负例保存在 examples/python-language/evidence/completion/additional/E08-wrong-wheel-hash.stderr;父目录成员、其他包前缀、重复成员和 RECORD 缺条目的负例保存在 examples/python-language/evidence/completion/native/e08-wheel-negative.stdout。累计回归的最终状态以 examples/python-language/evidence/completion/acceptance/ 中的本轮日志为准;PyPy 的剩余限制见 E04。没有调用 git push、部署服务或向 PyPI 发布。在强制杀进程后,读到成功状态也不能证明外部副作用已经执行,需按 35 的恢复边界 查证。
预测题。 传给服务 {"op":"submit","kind":"shell","text":"a"},随后让客户端查询已知任务,是否可能因这个非法请求增长任务编号?答案:不会。未知类型由 submit 白名单校验拒绝,实验核对错误响应及 task_ids 不变;先前的合法任务照常可查询。不能把报错消息本身当成不变性证据。
变更题。 把 read_fixture 的目标检查改为 str(target).startswith(str(root)),根目录为 /tmp/jobs,目标 /tmp/jobs-secret/leak.txt 会怎样?答案:字符串前缀误判为根内。Path.is_relative_to(root.resolve()) 会拒绝兄弟目录;现有测试还会拒绝根内指向根外的符号链接。即使通过这些测试,攻击者能在 resolve 与 open 之间替换目录项时仍有竞态,验收仅适用于当前受控本地文件系统实验。
参考资料
- Python JSON 解码、pickle 安全警告、pathlib 路径解析:解析限制与不可信代码执行边界。
- Python Packaging User Guide:制品、依赖声明与安装环境是不同层次;本文没有将可选实验依赖并入 wheel。
examples/python-language/experiments/E08_audit.py、examples/python-language/evidence/completion/acceptance/wheel.json、installed-E08.stdout、examples/python-language/evidence/completion/native/e08-wheel-negative.stdout与examples/python-language/evidence/completion/environment/optional-lock.stdout:真实 wheel 哈希、成员路径和 RECORD 覆盖、畸形帧、路径及符号链接反例、可选依赖 hash lock 和累计运行记录。




