untar_dir() 实现的路径遍历(Tar Slip)严重程度: 严重,CVSS 3.1 9.8
向量(v3.1): CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
受影响版本: fastcore <= 2.2.30
修复版本: 撰写本文时尚未修复
CWE: CWE-22(对受限目录的路径名限制不当,即“路径遍历”)
组件: fastcore.xtras.untar_dir() → fastcore.xtras._unpack() → shutil.unpack_archive()
高风险运行时: Python < 3.14(Python 3.14 将安全的 data tar 过滤器设为默认值)
报告者: Rahul Karne
CNA: VulnCheck
fastcore.xtras.untar_dir(fname, dest) 是一个有文档记录的辅助函数,用于将归档文件解包“到 dest 中”。
在内部,它将归档文件交给标准库的 shutil.unpack_archive(),没有使用解压过滤器,没有验证归档成员路径,也没有任何解压后的包含性检查。
因此,如果 TAR 归档文件的成员包含 ../ 遍历序列或绝对路径,就可以在调用者要求 fastcore 解压到的目录之外写入文件。
调用者合理地期望 untar_dir(fname, dest) 将所有内容限制在 dest 内,但实际上却可以在进程有写入权限的任何位置进行任意文件写入。
在调用 untar_dir() 的进程权限下进行任意文件写入。
根据被覆盖的内容,可能导致:
概念验证端到端地演示了完整的写入 → 导入 → 代码执行链,且完全在一个无害的演示目录内进行。
fastcore 是整个 fast.ai 生态系统的基础依赖,包括:
fastainbdevghapi它每月有数千万次下载。
untar_dir() 是常见的“下载归档文件,然后解包”工作流背后的解压原语,该工作流与 fastai 的 untar_data 遵循相同的通用模式。
任何将内容不完全可信的归档文件传给 untar_dir() 的代码路径都面临风险。
存在漏洞的代码位于 fastcore/xtras.py:
def _unpack(fname, out):
import shutil
shutil.unpack_archive(str(fname), str(out)) # <-- no filter, no validation
ls = out.ls()
return ls[0] if len(ls) == 1 else out
def untar_dir(fname, dest, rename=False, overwrite=False, uid=-1, gid=-1):
"untar `file` into `dest` ..."
import tempfile, shutil
dest = Path(dest)
with tempfile.TemporaryDirectory() as d:
out = Path(d) / remove_suffix(Path(fname).stem, '.tar')
out.mkdir()
...
src = _unpack(fname, out) # extraction escapes `out` here
...
shutil.move(str(src), dest)
...
return dest
untar_dir() 创建一个内部临时解压目录:
<tempdir>/<random>/<archive-stem>/
然后调用 _unpack(),后者将归档文件直接转发给:
shutil.unpack_archive()
对于 TAR 归档文件,这最终会调用 tarfile.extractall()。
在 Python < 3.14 上,默认的解压行为是 fully_trusted,这意味着遍历成员会被接受。
例如,一个归档成员如:
../../../some/other/dir/file
会相对于解压目录写入,因此可以逃逸出该目录。
fastcore 中没有任何机制:
filter="data"。.. 路径组件。untar_dir() 将 shutil.unpack_archive() 视为一个安全的、沙箱化的归档解压器。
但它并不是。
Python 标准库文档警告说,解压不受信任的归档文件可能会在请求的目标位置之外创建文件,并建议对 TAR 归档文件使用安全的解压过滤。
fastcore 既没有传递适当的解压过滤器,也没有为其支持的 Python 版本实现等效的包含性防护。
它也没有就这一安全敏感行为向 untar_dir() 的调用者发出警告。
TAR 成员可以包含任意路径字符串。
在受影响的 Python 版本上,tarfile.extractall() 默认不会自动消除 .. 遍历序列。
因此,相对于 fastcore 的临时解压目录写入一个遍历成员,就足以逃逸该目录并在进程有写入权限的任何位置写入。
一个可靠的利用原语是相对遍历,例如:
../../../target/file
这就是概念验证所使用的技术。
危险的输入是归档文件的内容,而不是开发者选择的目标路径。
归档文件经常从外部或半可信位置获取,包括:
因此,任何从不受信任或半可信来源下载归档文件并随后使用 untar_dir() 处理它的服务或工具都可能受到影响。
PoC 还包含一个变体,从攻击者控制的 HTTP 端点提供精心构造的归档文件,以演示这种投递路径。
成功利用需要:
应用程序对内容受攻击者影响的归档文件(例如上传、下载或其他不受信任的归档文件)调用 fastcore.xtras.untar_dir()。
运行时使用 Python < 3.14,或其他未强制执行安全 TAR 解压过滤器的配置。
要从任意文件写入升级到代码执行,进程稍后需要从遍历可达的位置导入或加载文件,例如:
任意文件写入和配置/数据修改仅需要条件 1 和 2。
poc_fastcore_cna_demo.py 调用真实的 fastcore.xtras.untar_dir() API。
该演示有意设计为无害。
PoC 创建的每个文件都保留在通过以下参数指定的单个演示目录内:
--demo-root
PoC 不会修改真实的系统文件,也不会访问云服务或凭据。
打印环境详细信息以及 _unpack() 和 untar_dir() 的存在漏洞的源代码。
创建一个演示布局,包含:
service_app/plugins/
创建:
attacker_supplied_upload.tar
其中包含旨在指向预期解压根目录之外的归档成员,包括遍历成员以及:
service_app/plugins/startup_hook.py
def auto_extract_uploaded_archive(received_archive):
return untar_dir(
received_archive,
intended_root,
overwrite=True
)
验证文件被写入到预期解压目录之外。
通过导入归档文件写入的以下文件来模拟服务重载:
startup_hook.py
这个无害的启动钩子会创建:
SERVICE_RELOAD_MARKER
这演示了完整的链条:
Attacker-controlled archive
↓
Path traversal during extraction
↓
Arbitrary file write
↓
Write into importable / auto-loaded location
↓
Application loads attacker-controlled file
↓
Code execution
安装受影响的包:
pip install fastcore
该问题已通过以下版本确认:
fastcore 2.2.30
该演示可以针对以下任一方式运行:
fastcore 副本--source 选择的本地解压源代码树所有 PoC 写入都保留在通过 --demo-root 指定的目录内。
清理之前的 PoC 输出:
python poc_fastcore_cna_demo.py --demo-root ./poc_fastcore --clean
运行演示:
python poc_fastcore_cna_demo.py \
--demo-root ./poc_fastcore \
--source latest \
--no-pause
示例精简输出:
fastcore version: 2.2.30
Outside marker exists after extraction? True
Is outside marker inside intended extraction root? False
Relative traversal marker exists after extraction? True
Is relative traversal marker inside intended extraction root? False
Service plugin file exists after extraction? True
Service reload marker exists? True
POC WORKED: fastcore extracted attacker-controlled tar members outside the intended root.
在支持的 Python 版本上,使用安全的解压过滤器,例如:
filter="data"
配合 shutil.unpack_archive() 或相应的 tarfile 解压操作。
在解压前应检查每个归档成员。
拒绝:
.. 的路径链接目标也应在解压前进行解析和验证。
在写入前解析每个目标路径,并验证其保持在预期的解压根目录之下。
概念上:
resolved_member_path
↓
must remain inside
↓
resolved_extraction_root
如果解析后的目标路径逃逸出解压根目录,解压应失败。
回归测试至少应覆盖:
../ traversal
absolute paths
Windows drive-letter paths
UNC paths
symbolic links
hard links
明确记录 untar_dir() 是否旨在安全地处理不受信任的归档文件。
如果故意支持不安全的解压,安全行为仍应是默认值。
不安全行为应需要显式选择加入,而不是静默地信任归档文件内容。
在包级别修复发布之前:
untar_dir()。底层技术缺陷是由归档路径遍历引起的任意文件写入漏洞。
然而,最终严重程度取决于部署环境以及 untar_dir() 的暴露方式。
一个代表性的向量可能接近:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
结果:
9.8 — Critical
如果利用反而需要开发者手动下载并解压归档文件,则用户交互和攻击复杂度可能更高。
因此,最终的 CVSS 评分会显著降低。
在评估该问题时,应考虑两个重要因素。
这是与以下 CVE 相关的、早已为人所知的不安全 TAR 解压漏洞类别的一个实例:
CVE-2007-4559
现代 Python 版本已引入更安全的归档解压行为。
Python 3.14 将安全的 data 解压过滤器设为默认值。
因此,主要受影响的环境是 Python < 3.14。
然而,这些运行时仍然被广泛部署。
当调用者向 untar_dir() 提供内容受攻击者控制或其他不受信任的归档文件时,该漏洞就可被利用。
因此,本报告将归档文件的内容视为不受信任的安全边界。
一个健壮的归档解压辅助函数应无论归档文件如何获得都能安全失败。
将下面的占位日期替换为实际的披露日期。
YYYY-MM-DD —— 向维护者报告漏洞。YYYY-MM-DD —— 供应商回应 / 确认 / 无回应。2.2.30)中仍未修复;解压代码保持不变。由 Rahul Karne 发现并报告。
fastcore untar_dir() 文档
https://fastcore.fast.ai/xtras.html
fastcore xtras.py 源代码
https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py
PyPI 上的 fastcore
https://pypi.org/project/fastcore/
Python shutil.unpack_archive() 文档
https://docs.python.org/3/library/shutil.html#shutil.unpack_archive
Python tarfile 解压过滤器
https://docs.python.org/3/library/tarfile.html#extraction-filters
PEP 706 —— tarfile.extractall 的过滤器
https://peps.python.org/pep-0706/
CWE-22 —— 对受限目录的路径名限制不当
https://cwe.mitre.org/data/definitions/22.html
CVE-2007-4559 —— 不安全 TAR 解压漏洞类别
https://nvd.nist.gov/vuln/detail/CVE-2007-4559
本仓库记录了一项协调披露的安全发现,并提供了一个无害、自包含的概念验证。
PoC 仅在用户指定的 --demo-root 目录内写入,不执行任何破坏性操作。
演示中包含的任何网络功能仅限于可选的本地演示。
本材料仅供防御性安全研究和教育目的使用。
媒体咨询:[email protected]。完整的 PoC(攻击者服务器、遍历归档文件、受害者应用程序)及更多技术细节可应要求提供。