Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 706 – tarfile.extractall 过滤器

作者:
Petr Viktorin <encukou at gmail.com>
讨论至:
Discourse 帖子
状态:
最终版
类型:
标准跟踪
创建日期:
2023年2月9日
Python 版本:
3.12
发布历史:
2023年1月25日, 2023年2月15日
决议:
Discourse 消息

目录

重要

本 PEP 是一份历史文档。最新的规范文档现在可以在 tarfile 文档 中找到。

×

有关如何提出更改,请参阅 PEP 1

摘要

tarfile 中的解压方法将获得一个 filter 参数,该参数允许在解压存档时拒绝文件或修改元数据。提供了三个内置的命名过滤器,旨在限制可能令人惊讶或危险的功能。这些过滤器可以按原样使用,也可以作为自定义过滤器的基础。

经过一段弃用期后,一个严格(但更安全)的过滤器将成为默认值。

动机

tar 格式用于多种用例,其中许多用例有不同的需求。例如:

  • UNIX 工作站的备份应忠实地保留所有类型的详细信息,如文件权限、指向系统配置的符号链接以及各种特殊文件。
  • 解包数据包时,解包不会产生意外后果(例如通过将密码文件符号链接到公共位置来暴露密码文件)更为重要。

为了支持所有用例,tar 格式具有许多功能。在许多情况下,解压存档时最好忽略或禁用其中一些功能。

Python 允许使用 tarfile.TarFile.extractall() 解压 tar 存档,其文档警告 切勿在未经事先检查的情况下从不受信任的来源解压存档。但是,目前尚不清楚应该进行何种检查。事实上,正确进行这种检查非常棘手。因此,许多人要么不理会,要么检查不正确,导致了安全问题,例如 CVE-2007-4559

tarfile 首次编写以来,文档中的警告不足以被普遍接受。在可能的情况下,应该 明确请求 不安全操作;潜在危险的操作应该 看起来 危险。然而,TarFile.extractall 在代码审查中看起来是无害的。

Tarfile 解压也通过 shutil.unpack_archive() 暴露,这允许用户不必关心他们正在处理的存档类型。该 API 非常诱人,可以在未经事先检查的情况下解压存档,尽管文档再次警告不要这样做。

有人认为 Python 没错——它的行为与文档完全一致——但这与要点无关。我们应该改进情况,而不是推卸/避免责任。Python 及其文档是改进事物的最佳场所。

基本原理

我们如何改进事物?不幸的是,我们将需要更改默认值,这意味着破坏向后兼容性。TarFile.extractall 是人们需要解压 tarball 时会用到的。它的默认行为需要改变。

什么是最佳行为?这取决于用例。因此,我们将添加几个通用的“策略”来控制解压。它们基于 用例,理想情况下它们应该具有直接的安全含义:

  • 当前行为:信任存档。例如,适合作为库的构建块,这些库自己进行检查,或者解压您刚刚自己创建的存档。
  • 解压 UNIX 存档:大致遵循 GNU tar,例如从文件名中剥离前导 /
  • 解压通用数据存档:shutil.unpack_archive() 的用例,其中保留特定于 tar 或类 Unix 文件系统的详细信息并不重要。

经过一段弃用期后,最后一个选项——最受限但最安全的一个——将成为默认值。

即使有了更好的通用默认值,用户仍应验证他们解压的存档,并可能修改一些元数据。表面上看,以下是目前实现此目的的合理方法:

但是,这种方法存在一些问题:

  • 可以修改 TarInfo 对象,但对其的更改会影响同一 TarFile 对象上的所有后续操作。此行为对于大多数用途来说都很好,但尽管如此,如果 TarFile.extractall 默认这样做,那将是非常令人惊讶的。
  • 调用 getmembers 可能很昂贵,并且 需要可寻址的存档
  • 在提前验证成员时,可能需要跟踪每个成员如何更改文件系统,例如符号链接的设置方式。这很难。我们不能指望用户来做。

为了解决这些问题,我们将:

  • 提供一种支持的“克隆”和修改 TarInfo 对象的方法。类似于 dataclasses.replace()namedtuple._replacereplace 方法应该可以解决问题。
  • extractall 的循环中提供一个“过滤器”钩子,可以在处理成员之前修改或丢弃成员。
  • 要求此钩子在提取每个成员之前调用,以便它可以扫描磁盘的 当前 状态。这将大大简化策略的实现(在标准库和用户代码中),代价是无法进行精确的“空运行”。

钩子 API 将与 TarFile.add 现有的 filter 参数非常相似。我们也将它命名为 filter。(在某些情况下,“策略”可能是更合适的名称,但该 API 可以用于安全策略之外的更多用途。)

上面描述的内置策略/过滤器将使用公共过滤器 API 实现,因此它们可以用作构建块或示例。

开创先例

如果其他存档提取库(例如 zipfile)获得类似功能,它们应该尽可能模仿此 API。

为了简化用例,内置过滤器将具有字符串名称;例如,用户可以传递 filter='data' 而不是处理 TarInfo 对象的特定函数。

shutil.unpack_archive() 函数将获得一个 filter 参数,它将传递给 extractall

添加适用于各种存档格式的基于函数的 API 超出了本 PEP 的范围。

完整披露与分销商信息

PEP 作者为 Red Hat 工作,Red Hat 是 Python 的一个分销商,与 CPython 通常具有不同的安全需求和支持周期。此类分销商可能希望进行供应商补丁以:

  • 允许在系统范围内配置默认值,并且
  • 尽快更改默认值,即使在较旧的 Python 版本中也是如此。

该提案使这变得容易,并允许用户查询设置。

规范

修改和遗忘成员元数据

TarInfo 类将获得一个新的方法 replace(),它的工作方式与 dataclasses.replace 类似。它将返回 TarInfo 对象的副本,其中属性根据仅关键字参数指定进行替换:

  • 名称
  • mtime
  • 众数
  • linkname
  • uid
  • gid
  • uname
  • gname

除了 namelinkname 之外,任何这些参数都将被允许设置为 None。当 extractextractall 遇到这样的 None 时,它将不会设置该元数据。(如果 unamegnameNone,它将回退到 uidgid,就像未找到名称一样。)当 addfiletobuf 遇到这样的 None 时,它将引发 ValueError。当 list 遇到这样的 None 时,它将打印一个占位符字符串。

文档将提及该方法存在的原因:从 TarFile.getmembers 检索的 TarInfo 对象是“活动”的;直接修改它们会影响后续不相关的操作。

过滤器

TarFile.extractTarFile.extractall 方法将新增一个 filter 仅关键字参数,该参数接受一个可调用对象,可以按如下方式调用:

filter(/, member: TarInfo, path: str) -> TarInfo|None

其中 member 是要提取的成员,path 是存档提取到的路径(即,对每个成员都相同)。

使用时,它将在每个成员被提取时调用,并且提取将根据结果进行。如果它返回 None,则该成员将被跳过。

该函数也可以引发异常。这取决于 TarFile.errorlevel,可以中止提取或导致该成员被跳过。

注意

如果提取中止,存档可能会部分提取。用户有责任进行清理。

我们还将为常见用例提供一组默认值。除了函数之外,filter 参数可以是以下字符串之一:

  • 'fully_trusted':当前行为:按原样尊重元数据。如果用户完全信任存档,或实现自己的复杂验证,则应使用此选项。
  • 'tar':大致遵循 GNU tar 命令的默认值(作为普通用户运行时)
    • 从文件名中删除前导 '/'os.sep
    • 拒绝提取具有绝对路径的文件(在上述 / 剥离之后,例如 Windows 上的 C:/foo)。
    • 拒绝提取其绝对路径(在跟踪符号链接之后)将超出目标的文件。(请注意,GNU tar 而是延迟创建一些链接。)
    • 清除高模式位(setuid、setgid、sticky)和组/其他写入位(S_IWGRP|S_IWOTH)。(这是 GNU tar 默认值的一个近似值,该默认值通过当前的 umask 设置来限制模式。)
  • 'data':提取“数据”存档,禁止常见攻击向量但限制功能。特别是,许多特定于 UNIX 风格文件系统(或等效地,特定于 tar 存档格式)的功能被忽略,这使其成为跨平台存档的良好过滤器。除了 tar
  • 拒绝提取链接(硬链接或软链接),这些链接链接到绝对路径。
  • 拒绝提取最终链接到目标外部路径的链接(硬链接或软链接)。(在不支持链接的系统上,tarfile 在大多数情况下会回退到创建常规文件。此提案不更改该行为。)
  • 拒绝提取设备文件(包括管道)。
  • 对于常规文件和硬链接
  • 对于其他文件(目录),完全忽略模式(将其设置为 None)。
  • 忽略用户和组信息(将 uidgidunamegname 设置为 None)。

任何其他字符串都将导致 ValueError

相应的过滤函数将作为 tarfile.fully_trusted_filter()tarfile.tar_filter() 等可用,因此它们可以很容易地用于自定义策略。

请注意,这些过滤器从不返回 None。以这种方式跳过成员是用户定义过滤器的功能。

默认值及其配置

TarFile 将获得一个新的属性 extraction_filter,以允许配置默认过滤器。默认情况下,它将是 None,但如果 filter 参数缺失或为 None,用户可以将其设置为将被调用的可调用对象。

注意

此处不接受字符串名称。那样会鼓励诸如 my_tarfile.extraction_filter = 'data' 这样的代码。在没有此功能的 Python 版本上,这将什么也不做,默默地忽略一个与安全相关的请求。

如果参数和属性都为 None

  • 在 Python 3.12-3.13 中,将发出 DeprecationWarning,并且提取将使用 'fully_trusted' 过滤器。
  • 在 Python 3.14+ 中,它将使用 'data' 过滤器。

应用程序和系统集成商可能希望更改 TarFile 类本身的 extraction_filter 以设置全局默认值。当使用函数时,他们通常会希望将其包装在 staticmethod() 中,以防止注入 self 参数。

TarFile 的子类也可以覆盖 extraction_filter

FilterError

tarfile 模块中将添加一个新异常 FilterError。它将有几个新的子类,每个子类对应一个上述拒绝原因。FilterErrormember 属性将包含相关的 TarInfo

在上述列表中,“拒绝”提取文件意味着将引发 FilterError。与其他提取错误一样,如果 TarFile.errorlevel 大于或等于 1,这将中止提取;如果 errorlevel=0,错误将被记录并且成员将被忽略,但提取将继续。请注意,extractall() 可能会留下部分提取的存档;用户有责任进行清理。

错误级别,以及致命/非致命错误

目前,TarFile 有一个 errorlevel 参数/属性,它指定如何处理错误:

  • errorlevel=0 时,文档指出“使用 extract()extractall() 时,所有错误都会被忽略”。代码只忽略 非致命致命 错误(见下文),因此,例如,如果您将 None 作为目标路径传递,您仍然会收到 TypeError
  • errorlevel=1(默认值)时,所有 非致命 错误都会被忽略。(通过设置 debug 参数/属性,它们可能会被记录到 sys.stderr。)文档中没有定义哪些错误是 非致命 的,但代码将 ExtractionError 视为此类。具体来说,是以下问题:
    • “无法解析存档内的链接”(在不支持符号链接的系统上引发)
    • “系统不支持 fifo/特殊设备”(如果系统支持这些设备,例如对于 PermissionError,则不用于故障)
    • “无法更改所有者/模式/修改时间”

    请注意,例如,文件名过长磁盘空间不足 不符合条件。非致命 错误在类 Unix 系统上不太可能出现。

  • errorlevel=2 时,所有错误都会被引发,包括 致命 错误。哪些错误是 致命 的,再次没有定义;实际上是 OSError

过滤器拒绝提取成员不完全符合 致命/非致命 类别。

  • 本 PEP 不改变现有行为。(欢迎在 Discourse 主题 25970 中提出改进意见。)
  • 当过滤器拒绝提取成员时,错误默认不应静默通过。

为了满足此要求,FilterError 将被视为 致命 错误,也就是说,它只会在 errorlevel=0 时被忽略。

希望忽略 FilterError 但不忽略其他 致命 错误的用户应创建自定义过滤器函数,并在 try 块中调用另一个过滤器。

进一步验证的提示

即使进行了拟议的更改,tarfile 仍不适合在未经事先检查的情况下提取不受信任的文件。除其他问题外,拟议的策略不能阻止拒绝服务攻击。用户应进行额外检查。

新文档将告诉用户考虑

  • 提取到一个新的空目录,
  • 对磁盘、内存和 CPU 使用施加外部(例如,操作系统级别)限制,
  • 根据允许的字符列表检查文件名(以过滤掉控制字符、易混淆字符等),
  • 检查文件名是否具有预期的扩展名(不鼓励在“点击”时执行的文件,或没有扩展名的文件,如 Windows 特殊设备名称),
  • 限制提取文件的数量、提取数据的总大小以及单个文件的大小,
  • 检查在不区分大小写的文件系统上将被遮蔽的文件。

此外,文档将指出:

  • tar 文件通常包含同一文件的多个版本:提取时,后来的版本预计会覆盖较早的版本,
  • tarfile 不会防范“实时”数据的问题,例如,攻击者在提取(或添加)过程中篡改目标目录(有关更多信息,请参阅 GNU tar 手册)。

此列表并非详尽无遗,但文档是收集此类一般技巧的好地方。如果它变得太长或需要与 zipfileshutil 合并(超出本提案范围),则可以将其移至单独的文档。

TarInfo 身份和 offset

使用 replace() 的过滤器,提取机制处理的 TarInfo 对象不一定与 members 中存在的对象相同。这可能会影响覆盖 makelink 等方法并依赖对象身份的 TarInfo 子类。

此类代码可以切换到比较 offset,即成员头在文件中的位置。

请注意,可重写方法和 offset 都仅在源代码注释中有所记载。

tarfile CLI

CLI (python -m tarfile) 将获得一个 --filter 选项,该选项将接受所提供默认过滤器之一的名称。无法指定自定义过滤器函数。

如果未提供 --filter,CLI 将使用默认过滤器(目前为 'fully_trusted',并附带弃用警告;从 Python 3.14 开始为 'data')。

不会有短选项。(-f 会与 GNU tar 的文件名选项令人困惑地相似。)

其他存档库

如果其他归档库(例如 zipfile)获得类似功能,其解压函数应使用 filter 参数,该参数至少接受字符串 'fully_trusted'(应禁用任何安全预防措施)和 'data'(应避免可能让用户感到惊讶的功能)。

标准化基于函数的过滤器 API 超出了本 PEP 的范围。

Shutil

shutil.unpack_archive() 将获得一个 filter 参数。如果给定,它将传递给底层提取函数。目前,将其传递给 zip 存档将失败(直到 zipfile 获得 filter 参数,如果它真的获得的话)。

如果未指定 filter(或将其保留为 None),则它不会被传递,因此解压 tarball 将使用默认过滤器(现在是 'fully_trusted' 附带弃用警告,从 Python 3.14 开始是 'data')。

复杂过滤器

请注意,一些用户定义的过滤器需要,例如,计算提取的成员或进行后处理。这需要比 filter 可调用对象更复杂的 API。但是,这种复杂的 API 不需要暴露给 tarfile。例如,使用假设的 StatefulFilter,用户将编写:

with StatefulFilter() as filter_func:
    my_tar.extract(path, filter=filter_func)

一个简单的 StatefulFilter 示例将添加到文档中。

注意

需要有状态过滤器是反对允许注册自定义过滤器名称(除了 'fully_trusted''tar''data')的原因之一。有了这样的机制,(至少)设置和拆卸的 API 将需要固定下来。

向后兼容性

TarFile.extractTarFile.extractall 的默认行为将更改,在发出 DeprecationWarning 两个版本(Python 向后兼容性策略 中允许的最短弃用期)之后。

此外,依赖 tarfile.TarInfo 对象身份的代码可能会中断,请参阅 TarInfo 身份和 offset

向后移植和向前兼容性

此功能可能会向后移植到较旧的 Python 版本。

在 CPython 中,我们不在补丁发布中添加警告,因此默认过滤器应在向后移植中更改为 'fully_trusted'

除此之外,tarfile所有 更改都应该向后移植,因此 hasattr(tarfile, 'data_filter') 成为所有新功能可靠的检查。

请注意,CPython 的通常策略是避免在安全向后移植中添加新的 API。此功能在没有新的 API(TarFile.extraction_filterfilter 参数)的情况下没有意义,因此我们将破例。(有关详细信息,请参阅 Discourse 评论 23149/16。)

以下是考虑 tarfile 是否具有所提议功能的代码示例。

复制这些代码片段时,请注意设置 extraction_filter 将影响后续操作。

  • 完全受信任的存档
    my_tarfile.extraction_filter = (lambda member, path: member)
    my_tarfile.extractall()
    
  • 如果可用,使用 'data' 过滤器,但如果此功能不可用,则恢复到 Python 3.11 行为 ('fully_trusted')
    my_tarfile.extraction_filter = getattr(tarfile, 'data_filter',
                                           (lambda member, path: member))
    my_tarfile.extractall()
    

    (这是一个不安全的操作,因此应明确说明,最好附带注释。)

  • 使用 'data' 过滤器;如果不可用则 失败
    my_tarfile.extractall(filter=tarfile.data_filter)
    

    my_tarfile.extraction_filter = tarfile.data_filter
    my_tarfile.extractall()
    
  • 使用 'data' 过滤器;如果不可用则 警告
    if hasattr(tarfile, 'data_filter'):
        my_tarfile.extractall(filter='data')
    else:
        # remove this when no longer needed
        warn_the_user('Extracting may be unsafe; consider updating Python')
        my_tarfile.extractall()
    

安全隐患

该提案以牺牲向后兼容性为代价提高了安全性。特别是,它将帮助用户避免 CVE-2007-4559

如何教授此内容

API、使用说明和进一步验证的提示将添加到文档中。这些应该可供熟悉一般存档但对 UNIX 文件系统特性及相关安全问题不熟悉的用户使用。

参考实现

参见 GitHub 上的 pull request #102953

被拒绝的想法

SafeTarFile

Lars Gustäbel 最初的想法是提供一个单独的类来实现安全检查(参见 gh-65308)。这种方法存在两个主要问题:

  • 这个名字具有误导性。一般的存档操作永远不可能完全“安全”,免受所有不必要的行为,而不影响合法用例。
  • 它没有解决不安全默认值的问题。

然而,SafeTarFile 背后的许多想法在本 PEP 中得到了重用。

向 tarfile 添加 absolute_path 选项

Issue gh-73974 要求在解压方法中添加 absolute_path 选项。这将是正式解决 CVE-2007-4559 的最小更改。它不足以保护不知情的人,也无法赋能勤奋和好奇的人。

'tar' 过滤器的其他名称

'tar' 过滤器暴露了类 UNIX 文件系统特有的功能,因此它可以命名为 'unix'。或者 'unix-like''nix''*nix''posix'

从功能上讲,tar 格式类 UNIX 文件系统 本质上是等效的,因此 tar 是一个好名字。

可能需要进一步的工作

向 zipfile 和 shutil.unpack_archive 添加过滤器

为了一致性,zipfileshutil.unpack_archive() 可以获得对 filter 参数的支持。然而,这需要本 PEP 作者无法承诺在 Python 3.12 中进行的研究。

zipfile 的过滤器可能对安全没有帮助。Zip 主要用于跨平台数据包,因此,ZipFile.extract 的默认值已经类似于 'data' 过滤器所做的事情。'fully_trusted' 过滤器会 新允许 绝对路径和 .. 路径组件,除了统一的 unpack_archive API 之外,可能没有太多用处。

过滤器应该对安全以外的用例有用,但这些通常需要自定义过滤器函数,并且需要适用于 TarInfoZipInfo 的 API。这 绝对 超出了本 PEP 的范围。

如果只实现了本 PEP 而 zipfile 没有变化,那么 unpack_archive 调用者的影响是 tar 文件的默认值从 'fully_trusted' 更改为更合适的 'data'。在此过渡期内,Python 3.12-3.13 将发出 DeprecationWarning。这很烦人,但有几种处理方法:例如,有条件地添加 filter 参数,全局设置 TarFile.extraction_filter,或忽略/抑制警告直到 Python 3.14。

此外,由于许多对 unpack_archive 的调用可能不安全,因此希望 DeprecationWarning 经常会成为审查受影响代码的有用提示。

致谢

本提案基于许多人的前期工作和讨论,特别是 Lars Gustäbel、Gregory P. Smith、Larry Hastings、Joachim Wagner、Jan Matejek、Jakub Wilk、Daniel Garcia、Lumír Balhar、Miro Hrončok 等。


来源:https://github.com/python/peps/blob/main/peps/pep-0706.rst

最后修改:2025-02-01 08:55:40 GMT