首页
/ openai-agents-python 沙箱工作区安全 Tar 归档工具 tar_utils 深度解析

openai-agents-python 沙箱工作区安全 Tar 归档工具 tar_utils 深度解析

2026-09-10 10:19:23作者:段琳惟

tar_utils 是 openai-agents-python 沙箱子系统中负责工作区(workspace)tar 归档安全打包、校验与解包的底层工具模块,对应参考文档 docs/ref/sandbox/util/tar_utils.md,实际实现位于 src/agents/sandbox/util/tar_utils.py。无论是本地 unix_local 沙箱的持久化与恢复,还是 Docker 沙箱把容器内工作区快照传回宿主机,都会经过这组函数。读完本文,你将掌握:tar 成员路径的校验规则、符号链接与硬链接的处理策略、归档校验的全部参数及其语义,以及安全解包时文件权限恢复与写入顺序的底层设计。

为什么沙箱需要一套“安全 tar 工具”

沙箱 Agent 的运行离不开工作区(workspace)目录:Agent 在其中读写文件、执行命令,会话结束后需要把整个目录打包成 tar 流持久化保存;恢复会话时再把 tar 流解包回工作区。问题在于,tar 归档是一种“可被恶意构造”的容器——一个来历不明的归档可能包含:

  • 绝对路径成员(/etc/passwd),解包时直接写到目标根目录之外;
  • 父目录穿越路径(../../etc/passwd);
  • Windows 驱动器路径(C:\...)或反斜杠分隔符(\evil.txt);
  • 符号链接、硬链接,或“归档内的符号链接之下再嵌套其他成员”,从而借链接逃逸目标目录;
  • 设备文件、FIFO 等非普通文件类型。

如果沙箱后端不加甄别地把这类归档解包到工作区根目录,就可能造成宿主机文件被覆盖或泄露。tar_utils 正是这一安全边界的实现者:它以一套统一的“安全策略”校验归档,再执行受控解包。

从源码结构看,模块的公共 API 非常收敛,只有 1 个异常类 + 4 个函数

符号 职责
UnsafeTarMemberError 安全校验失败的专用异常,携带 member(成员名)与 reason(失败原因)
safe_tar_member_rel_path 校验单个 tar 成员,返回相对路径(根目录成员返回 None
validate_tarfile / validate_tar_bytes 对已打开或原始字节的 tar 归档执行完整安全校验
safe_extract_tarfile 在安全策略约束下把归档解包到指定根目录
strip_tar_member_prefix 重写 tar 流,把成员的前缀目录替换为 .(工作区快照归一化)
should_skip_tar_member 依据工作区相对路径前缀判断成员是否应被跳过

其中 UnsafeTarMemberError 继承自 ValueError,构造时统一生成形如 unsafe tar member '...': <reason> 的报错信息,并分别把 memberreason 存为实例属性,便于上层(如 archive_extraction.py)将其转换为带上下文的 WorkspaceArchiveWriteError

成员路径校验:safe_tar_member_rel_path

这是整个模块的“地基”函数。它接收一个 tarfile.TarInfo 成员,返回该成员相对工作区根的 Path;若成员本身是归档根(名称为空、../),则返回 None。其校验顺序与规则如下:

  1. 根成员校验:若成员名称是 """.""./",则调用 _validate_archive_root_member——只有目录型成员可以通过;根位置的符号链接、硬链接或普通文件一律抛出 UnsafeTarMemberError(分别对应 archive root symlinkarchive root hardlinkarchive root member must be directory)。
  2. Windows 路径拦截:用 PureWindowsPath 解析成员名,若存在驱动器号(如 C:)或成员名包含 \,直接拒绝(windows drive path / windows path separator)。这一步防止在 Linux 上看似合法、但换到 Windows 语义下可逃逸的路径混入。
  3. 绝对路径拒绝PurePosixPath(member.name).is_absolute() 为真时抛出 absolute path
  4. 父目录穿越拒绝:成员路径任一分量为 .. 时抛出 parent traversal
  5. 链接类型策略:默认(allow_symlinks=False)拒绝符号链接成员(symlink member not allowed);任何硬链接成员都被拒绝(hardlink member not allowed)。
  6. 成员类型白名单:只有目录、普通文件(以及显式放行时的符号链接)是合法类型,设备、FIFO 等一律 unsupported member type

函数签名中的 allow_symlinks: bool = False 是一个重要的“opt-in”开关:常规场景(如归档解包到沙箱工作区)要求符号链接显式放行,因为 Python 虚拟环境等正常开发工作区中天然存在符号链接。测试 test_tar_utils.py 中的 test_safe_tar_member_rel_path_requires_symlink_opt_in 精确验证了这一行为:默认调用抛 symlink member not allowed,传 allow_symlinks=True 后才返回 Path("link.txt")

归档级校验:validate_tarfile 与 validate_tar_bytes

单成员校验之外,还需要对整个归档的成员间关系做一致性检查。validate_tarfile 接收一个已打开的 tarfile.TarFilevalidate_tar_bytes 则直接接收原始字节(内部用 io.BytesIO 打开后委托给前者),两者共享同一套策略,参数完全一致:

参数 默认值 语义
reject_rel_paths () 受保护路径集合;归档中任何成员与该集合中任一前缀“重叠”即拒绝
reject_symlink_rel_paths () 指定的符号链接相对路径集合,匹配即拒绝该符号链接成员
skip_rel_paths () 需跳过的路径前缀集合,命中成员直接跳过(不参与后续校验)
root_name None tar 打包时可能带有的工作区根目录名,用于识别成员名的两种变体
allow_symlinks True 是否放行符号链接成员(validate_tar_bytes 无此参数,固定放行)
allow_external_symlink_targets True 是否允许符号链接目标指向归档外部(绝对路径或 .. 逃逸)

核心校验逻辑分两阶段:

阶段一:逐成员扫描。 每个成员先经过 should_skip_tar_member 判断是否应跳过;未跳过的成员调用 safe_tar_member_rel_path 取得相对路径(allow_symlinks 传参透传)。随后执行:

  • 受保护路径重叠检查:对 reject_rel_paths 中每个前缀,若成员路径“落在该前缀内”(_is_within),或成员非目录而该前缀落在成员路径内,则抛出 archive member overlaps protected path: ...。这能同时拦截“写入受保护目录”和“把受保护路径变成普通文件”两种攻击。测试中的 test_validate_tar_bytes_rejects_members_overlapping_protected_pathtest_validate_tar_bytes_allows_directory_ancestor_of_protected_path 分别验证了拒绝与放行边界。
  • 重复路径检查:同一相对路径出现两次(且非“两个目录”这种安全叠加)即报 duplicate archive path
  • 符号链接目标检查:当 allow_external_symlink_targets=False 时,符号链接目标必须是相对路径,且从成员自身所在目录解析后仍位于归档根内——绝对目标报 absolute symlink target not allowed,越界相对目标(如 ../../etc/passwd)报 symlink target escapes archive roottest_validate_tar_bytes_allows_internal_symlink_target_in_strict_mode 证明 ../bin/python3 这类“归档内部”的链接在严格模式下依然合法。
  • 符号链接路径黑名单:命中 reject_symlink_rel_paths 时抛出 symlink member not allowed: <path>;路径会先经 _normalize_rel 归一化(去掉 ../ 前缀),所以 "./workspace""workspace" 等价(见 test_validate_tar_bytes_specific_symlink_rejection_normalizes_dot_prefix)。

阶段二:成员间关系检查。 归档解包时成员可能有先后顺序问题:如果归档先写一个符号链接 escape -> /tmp/outside,再写 escape/pwned.txt,后者就会穿透链接写到根目录外。因此第二阶段对每个成员遍历其所有父路径:

  • 若某个父路径是归档中的符号链接成员,抛出 archive path descends through symlink: ...(对应测试 test_validate_tar_bytes_rejects_members_under_archive_symlink);
  • 若某个父路径在归档中是普通文件(非目录),抛出 archive path descends through non-directory: ...(对应测试 test_validate_tar_bytes_rejects_member_under_non_directory_member)。

validate_tar_bytes 还有一层兜底:任何 tarfile.TarErrorOSError 都会被包装为 UnsafeTarMemberError(member="<tar>", reason="invalid tar stream"),保证调用方只需捕获单一异常类型。

安全解包:safe_extract_tarfile

校验通过后,解包由 safe_extract_tarfile(tar, *, root, allow_external_symlink_targets=True) 执行。它在 validate_tarfile(此处 allow_symlinks 固定为 True,因为工作区快照需要保留符号链接)的基础上,进一步处理了既有文件系统状态带来的风险:

  1. 预先存在的符号链接父路径检查_ensure_no_symlink_parents):解包前把目标路径 resolve() 后与根目录比对,若解析结果逃出根目录(说明路径中已存在指向外部的符号链接组件),立即拒绝,reason 为 path escapes root after resolutionsymlink in parent path。测试 test_safe_extract_tarfile_rejects_preexisting_symlink_parent 用“根目录下预先放置指向外部的符号链接 escape,再解包 escape/pwned.txt”验证了这一点,并断言外部目录未被写入。
  2. 叶子节点替换规则:解包目标若已存在目录,且新成员是符号链接,则拒绝(destination directory already exists,见 test_safe_extract_tarfile_rejects_existing_leaf_directory_for_symlink);其余情况(文件↔符号链接、文件↔目录)允许替换——先 unlink 再创建,保证不会“穿透”既有符号链接写入。
  3. 文件写入使用 O_NOFOLLOW:打开目标文件时使用 os.O_WRONLY | os.O_CREAT | os.O_EXCL,并尽量加上 O_NOFOLLOW,从系统调用层面杜绝跟随符号链接写入。
  4. 权限恢复策略_restored_regular_file_mode):镜像 Python 标准库 tarfiledata filter 模式策略——丢弃 setuid/setgid/sticky 位以及 group/other 的写权限;只有属主具有执行位时才保留执行位;属主始终可读写。即 mode & 0o755 之后再保证 0o600 底线。这样既能让快照中的可执行脚本(如 .venv/bin/python)保持可执行,又不会把危险的权限位带入恢复环境。测试 test_tar_utils.pytest_safe_extract_tarfile_restores_regular_file_modes 用 11 组参数化用例逐一验证:0o777 → 0o755(去掉 group/other 写位)、0o4755 → 0o755(去 setuid)、0o444 → 0o644(保持属主可写)、0o000 → 0o600(保持属主可读)等。
  5. 部分写入文件保持私有:写入时先以 0o600 创建,fchmod 恢复权限放在 payload 写完并 flush 之后——若拷贝中途失败,残留文件保持 0o600 私有模式而非最终可读/可执行模式。test_safe_extract_tarfile_keeps_a_partially_written_file_private 用“读出一个 chunk 后抛错”的模拟 payload 验证了失败路径下文件既非可执行、权限仍为 0o600
  6. 两阶段写入顺序:第一遍先处理目录与普通文件(符号链接成员跳过),第二遍才创建符号链接——确保任何符号链接都建立在“目录和普通文件已就位”的文件系统之上,避免符号链接被后续成员当作父路径利用。

该函数返回 None,失败时统一抛出 UnsafeTarMemberError

前缀归一化:strip_tar_member_prefix

不同打包后端产生的工作区 tar,成员前缀可能各不相同。例如 Docker 把工作区先拷贝到 /tmp/stage/workspace 再打包,得到的成员名是 workspace/...;而可移植的工作区快照应当与“源后端根目录名”无关,统一存成 .... 的相对形式。

strip_tar_member_prefix(data, *, prefix) 完成这一转换:它流式读取输入(mode="r|*" 自动识别压缩格式),逐成员重写后写出(mode="w|"),把 prefix 前缀替换为 .

  • 根成员(返回 None)或恰好等于 prefix 的成员 → 重命名为 "."
  • prefix 为前缀的成员 → 截掉前缀部分;
  • 其余成员 → 抛出 member does not start with prefix

重写时通过 copy.copy(member) 保留元数据,并清理 PAX 扩展头中的 path 字段(避免长文件名场景下旧路径残留)。测试 test_strip_tar_member_prefix_returns_workspace_relative_archive 验证输入 workspace/pkg/main.py 等成员后输出为 [".", "pkg", "pkg/main.py", "pkg/python"]test_strip_tar_member_prefix_rewrites_pax_path_headers 则验证超长文件名(>100 字符,PAX 格式)下 pax_headers["path"] 被同步改写。函数返回一个 seekable 的新流,且在返回前会再次调用 validate_tarfile 对重写结果做整体校验。

成员跳过:should_skip_tar_member

打包时某些路径(如运行时缓存 .runtime、敏感挂载点)不应进入快照。should_skip_tar_member(member_name, *, skip_rel_paths, root_name) 依据“工作区相对前缀”判断成员是否应排除:由于 member_name 可能带 . 前缀或工作区根目录名,函数先通过 _tar_member_rel_variants 生成路径的多个变体(原始形式与去掉 root_name 后的形式),只要任一变体落在任一 skip_rel_paths 前缀内(_is_within)即返回 True。测试 test_validate_tar_bytes_ignores_skipped_unsafe_member 展示了跳过语义:即使被跳过的成员本身是危险符号链接(.runtime/escape -> /tmp/outside),只要声明了 skip_rel_paths=[Path(".runtime")],校验即通过。

在仓库中的真实调用链

tar_utils 不是孤立工具,它是沙箱“工作区持久化(persist)→ 传输 → 恢复(hydrate)”闭环的安全核心,各后端调用方式如下:

本地沙箱(unix_local),见 src/agents/sandbox/sandboxes/unix_local.py

  • persist_workspace:用 tar.add(root, arcname=".", filter=...) 打包,filter 回调中调用 should_skip_tar_memberroot_name=None)跳过应排除的路径;
  • hydrate_workspace:调用 safe_extract_tarfile(tar, root=root, allow_external_symlink_targets=False)——本地恢复采用严格模式,禁止符号链接指向归档外部。

Docker 沙箱,见 src/agents/sandbox/sandboxes/docker.py

  • persist_workspace:先在容器内把工作区拷到 staging 目录,打包后调用 strip_tar_member_prefix(root_prefixed_archive, prefix=staging_workspace.name),把 workspace/... 归一化为 ./...,再交给上层存储;
  • hydrate_workspace:先把输入流完整读入临时文件,再以 validate_tarfile(tar, allow_external_symlink_targets=False) 做严格校验,之后才通过容器 exec 落盘。

远程/容器内提取器,见 src/agents/sandbox/session/archive_extraction.pyWorkspaceArchiveExtractor.extract_tar_archive 逐成员调用 safe_tar_member_rel_path,并在写盘前用 ls 结果检查目标父路径中是否已存在符号链接(_ensure_no_symlink_extract_parents),所有 UnsafeTarMemberError 都被转换为带 member/reason 上下文的 WorkspaceArchiveWriteError

此外,远程沙箱扩展(如 src/agents/extensions/sandbox/ 下的 daytona、runloop、vercel、e2b、modal、blaxel、cloudflare 等后端)也引用本模块,共享同一套安全策略,保证不同后端间工作区快照的语义一致。

测试如何锁定安全边界

tests/sandbox/test_tar_utils.py 是本模块安全行为的“契约文档”,核心用例覆盖了本文提到的所有规则,可归纳为几组:

  • 路径攻击:根符号链接(archive root symlink)、Windows 驱动器路径(C:/...C:\...)、Windows 反斜杠分隔符(..\evil.txt\evil.txtnested\evil.txt)、绝对路径、.. 穿越;
  • 链接攻击:归档符号链接下嵌套成员(descends through symlink)、严格模式下绝对符号链接目标、父目录逃逸目标、硬链接与 FIFO(rejects_unsupported_tar_member_types);
  • 受保护路径:成员与 reject_rel_paths 重叠的拒绝、非目录祖先的拒绝、目录祖先的放行、指定符号链接路径的定向拒绝及其 . 前缀归一化;
  • 解包安全:预先存在的符号链接父路径、归档内符号链接目标重放(同路径先符号链接后文件、先文件后符号链接、先目录后文件等叶子替换矩阵)、目标目录已存在时拒绝符号链接;
  • 权限与失败路径:11 组 POSIX 模式恢复断言、脚本保持可执行(bin/start 可执行而 README.md 不可执行)、替换已存在文件时恢复新模式、部分写入失败后文件保持 0o600 私有。

这些测试同时验证了 validate_tar_bytessafe_extract_tarfilestrip_tar_member_prefix 三个入口的协同行为,是理解模块语义最直接的补充资料。

小结:一套可移植的安全归档策略

总结 tar_utils 的安全策略,可以浓缩为一张“拒绝清单”:

类别 拒绝项
路径形式 绝对路径、.. 穿越、Windows 驱动器/反斜杠路径、重复路径
成员类型 根位置非目录成员、硬链接、设备/FIFO 等非普通类型、默认配置下的符号链接
链接行为 归档内符号链接之下嵌套成员、严格模式下指向归档外部的符号链接目标、命中 reject_symlink_rel_paths 的链接
文件系统状态 预先存在且指向根外的符号链接父路径、目标位置已存在目录却要写符号链接
权限 setuid/setgid/sticky 与 group/other 写位一律清除,属主始终可读写

整套实现以“统一异常 + 统一策略 + 流式处理”为设计骨架:UnsafeTarMemberError 让所有后端只需捕获一种异常;validate_tarfile / validate_tar_bytes / safe_extract_tarfile / strip_tar_member_prefix 四个公共函数分别覆盖“校验已打开归档、校验原始字节、安全解包、前缀归一化”四种典型场景。无论是自己实现自定义沙箱后端,还是审查现有后端的工作区快照链路,这份源码与配套测试都是可直接对照的安全参考。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
927
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
603
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
396
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
527