登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python tarfile 解包怎样限制链接路径

来源:17golang原创

时间:2026-11-03 12:18:22 196浏览 收藏

如果 Python 程序要解包来源不完全可信的 tar 文件,建议在 Python 3.12 及以上显式传入 filter="data",再用一个自定义过滤器把符号链接和硬链接全部跳过。这样既能限制绝对路径、目标目录外路径和特殊文件,也能把“链接是否允许”变成明确的业务策略,而不是依赖运行环境的默认值。

官方文档:https://docs.python.org/3/library/tarfile.html

只检查成员名称里的 .. 还不够。链接的目标、目标目录的真实路径和特殊文件类型都要纳入解包策略;发生过滤异常后,目标目录可能只解出了一部分内容,不能把它当成完整结果继续使用。
  • Python 3.12 引入 extract() 和 extractall() 的 filter 参数。
  • filter="data" 会拒绝绝对路径、越界链接和设备文件,并限制部分元数据。
  • 如果业务根本不需要链接,额外返回 None 跳过符号链接和硬链接更直观。
  • Python 3.14 起默认过滤器改为 data,但显式写出策略有助于兼容 3.12、3.13 和代码审查。

先区分“路径越界”和“链接不允许”

tar 成员至少有两层需要处理:成员自身的名字,以及符号链接或硬链接的目标。成员名字为绝对路径、规范化后落到目标目录之外,属于路径边界问题;链接目标可以把一个看似位于解包目录里的条目指向别处,属于链接边界问题。两者不是同一个判断,不能只用字符串前缀或只查找 ../。

Python 官方的 data 过滤器已经覆盖常见的数据包场景:拒绝绝对路径、拒绝解析后落到目标目录外的链接、拒绝设备文件,并收紧权限和所有者元数据。若业务需要保留内部软链接,应先使用这一层,再在自定义过滤器中允许明确的例外。

tarfile data过滤器限制成员路径和链接目标的静态关系图
图中把成员名称、目标目录和链接目标放在不同边界内,帮助区分路径越界与链接策略。

先显式使用 data 过滤器

对于普通数据包、上传附件或构建产物,最小改动是给 extractall() 明确传入 filter="data"。这不会把 tar 解包变成完整的恶意文件检测器,但能把标准库已经定义的路径、链接和特殊文件限制应用到当前调用点。

from pathlib import Path
import tarfile

archive_path = Path("incoming/package.tar.gz")
target_dir = Path("work/unpacked")
target_dir.mkdir(parents=True, exist_ok=True)

try:
    with tarfile.open(archive_path, mode="r:*") as archive:
        # 显式选择数据包策略,避免依赖不同 Python 版本的默认值。
        archive.extractall(path=target_dir, filter="data")
except tarfile.FilterError as exc:
    # 过滤失败后目录可能只有部分内容,后续流程不要继续读取它。
    print(f"拒绝解包成员:{exc}")
    raise

这里的 r:* 允许按归档头自动识别常见压缩格式;真正决定链接和路径边界的是 filter 参数。FilterError 表示成员被策略拒绝,异常发生后应删除或隔离 target_dir,再由上层决定是否重试。

不需要链接时,用自定义过滤器全部跳过

如果解出来的是源码包、数据交换包或临时构建输入,通常不需要保留软链接和硬链接。可以先调用官方 data_filter(),再对返回的 TarInfo 检查类型;返回 None 表示跳过当前成员。这样不会重新实现路径规范化和越界判断。

from pathlib import Path
import shutil
import tarfile


def regular_files_only(member: tarfile.TarInfo, destination: str):
    # 先复用官方规则,处理绝对路径、越界链接和特殊文件。
    safe_member = tarfile.data_filter(member, destination)
    if safe_member is None:
        return None

    # 当前业务只接收目录和普通文件,不保留软链接或硬链接。
    if safe_member.issym() or safe_member.islnk():
        return None
    return safe_member


archive_path = Path("incoming/data.tar")
target_dir = Path("work/data")
if target_dir.exists():
    # 清掉上一次失败留下的半成品,避免新旧文件混在一起。
    shutil.rmtree(target_dir)
target_dir.mkdir(parents=True)

try:
    with tarfile.open(archive_path, mode="r:*") as archive:
        # 自定义过滤器只增加“禁用链接”这一条业务规则。
        archive.extractall(target_dir, filter=regular_files_only)
except tarfile.FilterError as exc:
    # 解包失败时不把半成品目录交给后续业务。
    shutil.rmtree(target_dir, ignore_errors=True)
    raise RuntimeError("tar 包未通过解包策略") from exc

过滤器接收当前成员和目标目录路径,每个成员解包前都会调用。data_filter() 可能直接抛出 FilterError,而 issym() 与 islnk() 则负责表达“本业务不接受任何链接”。如果确实需要内部链接,不要简单删除这两个判断,而应给允许的目标建立更窄的规则。

tarfile自定义过滤器由官方data规则和禁用链接策略组成的静态关系图
自定义过滤器的核心是复用官方 data 规则,再叠加“只接收目录和普通文件”的业务边界。

链接路径限制应放在解包前的过滤点

extractall() 的过滤器是在每个成员真正落盘前调用的,适合做“允许、修改或拒绝”决策。相比先调用 getmembers() 扫一遍再解包,过滤点能在当前文件系统状态下处理成员,也不会把同一组 TarInfo 对象的修改意外带到后续操作。

但过滤器不是资源配额系统:它不会自动限制归档展开后的总字节数、文件数量、单文件大小或压缩炸弹风险。面对外部上传包,还要在业务层设置上传大小、成员数量、总解包预算,并把解包放到权限较低的隔离目录中。

兼容 Python 版本并处理半成品

filter 参数从 Python 3.12 开始提供,Python 3.14 起默认值改为 data。如果项目仍支持 3.11 或更早版本,直接传入 filter 会触发参数不兼容;更稳妥的做法是升级运行时,或者在旧版本使用经过充分审查的独立解包实现,不要假装旧版本拥有同样的标准库策略。

异常恢复也要单独设计。过滤器拒绝成员时,归档可能已经解出部分文件;成功路径应在全部解包完成后再把临时目录原子改名,失败路径删除临时目录,避免消费者读到半成品。

最后按业务需要选策略

场景建议原因
普通数据包filter="data"直接采用标准库的数据包边界
不需要任何链接data_filter() 加 issym()/islnk()把链接能力明确关闭
可信备份恢复只在来源和权限都可控时评估 fully_trusted保留 tar 元数据的代价更高
旧 Python 运行时优先升级,或使用独立审查过的解包层旧版本没有同一套 filter API

一句话总结:限制链接路径的入口是 extractall(filter=...),基础规则交给 data 过滤器,业务不需要链接时再返回 None 跳过软链接和硬链接;最后用临时目录、权限和资源配额补上标准库过滤器不负责的部分。

常见问题

filter="data" 会不会保证所有 tar 包都安全?

不会。它主要限制路径越界、危险链接和特殊文件,并收紧部分元数据;文件数量、展开总大小、业务内容和运行权限仍要由应用处理。

为什么不只判断成员名是否包含 ..?

因为绝对路径、规范化后的越界路径和链接目标是不同情况。字符串判断容易漏掉组合路径,也无法表达链接是否允许。

过滤器返回 None 会报错吗?

不会。对自定义 extraction filter 来说,返回 None 表示跳过当前成员;官方过滤器拒绝危险成员时通常会抛出 FilterError。

Python 3.11 如何复用这套写法?

3.11 没有 extraction filter 参数和同一套内置过滤器。应优先升级到支持该 API 的版本;如果必须兼容旧版本,要单独采用经过审查的解包层,并把版本差异写进部署约束。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>