使用Python安全地解压zip或tar文件

30

我正在尝试将用户提交的zip和tar文件解压到一个目录中。 zipfile的extractall方法的文档(tarfile的extractall方法类似)指出,路径可以是绝对路径或包含..路径以跳出目标路径。相反,我可以自己使用extract,像这样:

some_path = '/destination/path'
some_zip = '/some/file.zip'
zipf = zipfile.ZipFile(some_zip, mode='r')
for subfile in zipf.namelist():
    zipf.extract(subfile, some_path)

这是安全的吗?在这种情况下,存档文件是否有可能出现在some_path之外?如果有,我该如何确保文件永远不会落到目标目录之外?


从Python 2.7.4开始,zipfile.extract()方法禁止在沙盒之外创建文件。因此,自Python 2.7.4起,该方法是安全的。然而,tar档案仍存在漏洞。 - alexis
5个回答

45

注意: 从python 2.7.4开始,这对于ZIP档案不再是一个问题。详情请见答案底部。本回答重点讨论tar档案。

要确定路径指向的真实位置,请使用os.path.abspath()(但注意符号链接作为路径组件的限制)。如果你使用abspath来规范化你的zip文件中的路径,并且它不包含当前目录作为前缀,则表示它指向外部。

但是,您还需要检查从您的存档中提取的任何符号链接的(tar文件和Unix zip文件都可以存储符号链接)。如果您担心一种俗称的“恶意用户”会故意绕过您的安全措施,而不是仅仅安装在系统库中的应用程序,则这一点非常重要。

这就是前面提到的警告: 如果您的沙盒已经包含指向目录的符号链接,那么abspath将会被误导。即使符号链接指向了沙盒内部,也可能存在危险: 符号链接sandbox/subdir/foo -> ..指向sandbox,所以路径sandbox/subdir/foo/../.bashrc应该被禁止。最简单的方法是等到之前的文件已被提取并使用os.path.realpath()。幸运的是,extractall()接受一个生成器,所以这很容易实现。

由于您要求代码,这里有一个解释算法的片段。它不仅禁止将文件提取到沙盒外(这是所要求的),还禁止创建指向沙盒外位置的链接。在沙盒内部。我很想听听是否有人能够悄悄地通过它放行任何杂散的文件或链接。

import tarfile
from os.path import abspath, realpath, dirname, join as joinpath
from sys import stderr

resolved = lambda x: realpath(abspath(x))

def badpath(path, base):
    # joinpath will ignore base if path is absolute
    return not resolved(joinpath(base,path)).startswith(base)

def badlink(info, base):
    # Links are interpreted relative to the directory containing the link
    tip = resolved(joinpath(base, dirname(info.name)))
    return badpath(info.linkname, base=tip)

def safemembers(members):
    base = resolved(".")
    
    for finfo in members:
        if badpath(finfo.name, base):
            print >>stderr, finfo.name, "is blocked (illegal path)"
        elif finfo.issym() and badlink(finfo,base):
            print >>stderr, finfo.name, "is blocked: Symlink to", finfo.linkname
        elif finfo.islnk() and badlink(finfo,base):
            print >>stderr, finfo.name, "is blocked: Hard link to", finfo.linkname
        else:
            yield finfo

ar = tarfile.open("testtar.tar")
ar.extractall(path="./sandbox", members=safemembers(ar))
ar.close()
编辑:从Python 2.7.4开始,对于ZIP档案,这不再是一个问题:zipfile.extract()方法会禁止在沙盒之外创建文件。

注意:如果成员文件名是绝对路径,则会剥离驱动器/UNC共享点和前导(反)斜杠,例如:在Unix上,///foo/bar 变成 foo/bar ,在Windows上,C:\foo\bar 变成 foo\bar。所有成员文件名中的 ".." 组件将被删除,例如:../../foo../../ba..r 变成 foo../ba..r。在Windows上,非法字符(:<>|"?*)将被替换为下划线(_)。

tarfile类没有进行类似的清理,因此上述答案仍然适用。

我也是这么想的;但你仍然需要注意我所概述的漏洞:首先,归档文件包含一个符号链接到另一个目录,然后是一个使用该符号链接作为其路径的文件。 - alexis
1
realpath将转换提取的文件为其真实路径,因此您可能只需在提取后检查它即可。 - jterrace
1
根据自述文件,如果Archive.extract()检测到越界文件,它将引发异常。该异常将终止批量提取,并且没有恢复的方法。甚至似乎没有一种方法可以列出存档内容并逐个提取文件。这让我感到不满。 - alexis
我认为符号链接和硬链接的打印消息应该交换。 - Albert Villanova del Moral
糟糕,@Alb发现得不错。 - alexis
显示剩余5条评论

4
与普遍的说法相反,安全地解压文件在 Python 2.7.4 中仍未完全解决。extractall方法仍然存在危险,可能会直接或通过解压符号链接引起路径遍历攻击。以下是我的最终解决方案,可防止所有版本的 Python 中的这两种攻击,甚至包括 extract 方法存在漏洞的 Python 2.7.4 之前的版本:
import zipfile, os

def safe_unzip(zip_file, extract_path='.'):
    with zipfile.ZipFile(zip_file, 'r') as zf:
        for member in zf.infolist():
            file_path = os.path.realpath(os.path.join(extract_path, member.filename))
            if file_path.startswith(os.path.realpath(extract_path)):
                zf.extract(member, extract_path)

编辑1: 修复变量名称冲突。感谢Juuso Ohtonen。

编辑2: s/abspath/realpath/g。感谢TheLizzard。


2
避免使用zipfile作为参数名称,因为它与导入名称冲突:AttributeError: 'str' object has no attribute 'ZipFile'。解决方法是将zipfile参数重命名为例如zip_file - Juuso Ohtonen
谢谢您的评论。我修复了示例代码。最初我从我的项目中提取出来并编辑成独立的,但显然忘记测试它了。 - shellster
2
你为什么使用 os.path.abspath 而不是 os.path.realpath?使用 os.path.realpath 不更安全吗? - TheLizzard
好的,我会更新答案以反映该建议。realpath 显然调用 abspath,因此 realpath 应该足够了。 - shellster

3
使用ZipFile.infolist()/TarFile.next()/TarFile.getmembers()获取存档中每个条目的信息,规范化路径,自行打开文件,使用ZipFile.open()/TarFile.extractfile()获取条目的类似文件的内容,并自行复制条目数据。

4
这似乎很难确保我做得对,尤其是当你有像“../../../../subdir/../../something/file.txt”这样的文件时,目标位置应该在哪里?之前没有人提供处理这个问题的代码吗? - jterrace
3
没有人能够替你回答这个问题,因为只有你自己了解你的应用需求。 - Ignacio Vazquez-Abrams
4
我不同意。其他工具会自动为您完成此操作 - 例如,“tar”命令会自动删除绝对路径,除非您指定“--absolute-names”。 - jterrace
1
任何委派给 tar 的软件都必须遵守这一规定。这是你的软件。 - Ignacio Vazquez-Abrams
7
sigh 当你遇到一个无效或不允许的路径时,你有三个选择:1)尝试提取并捕获任何错误,2)提取到修改后的路径,3)不提取。我无法告诉你哪种策略适用于你的应用程序。 - Ignacio Vazquez-Abrams
1
@IgnacioVazquez-Abrams:当然,但为什么Python不给你这些选项呢?它显然可以。而且为什么“默认”选项明显是最糟糕的呢? - Timmmm

3

将zip文件复制到一个空目录中。然后使用os.chroot将该目录作为根目录。然后在那里解压缩。

或者,您可以使用-j标志调用unzip本身,该标志忽略目录:

import subprocess
filename = '/some/file.zip'
rv = subprocess.call(['unzip', '-j', filename])

subprocess模块在运行Python的每个平台上都可以使用,据我所知。但是如果你说的是MS Windows,那么有几个可用于处理zip文件的程序,比如INFO-zip。当然,特定的命令行需要根据你想要使用的程序进行调整。 - Roland Smith
你说得对,os.chroot 是特定于 UNIX 的。但是如果你搜索一下,你会发现类似于 Windows 的 chroot 应用程序。当然,在这种情况下真正的过度解决方案是在虚拟机中运行 unzip。 :-) - Roland Smith
2
这是一个非常简单而聪明的想法,但是 (a) 它只在 Unix 系统上真正起作用,(b) 在 Unix 上,只有超级用户才能进行 chroot。在处理潜在不安全数据时进行特权升级确实是错误的做法... - alexis
使用info-zip的unzip命令的-j标志作为chroot的替代方法,应该适用于任何支持unzip的平台。 - Roland Smith

1
PSA:这个问题的被接受答案已经过时!
从Python 3.11.4版本开始,tarfile.TarFile.extractall()中包含了一个提取过滤机制。当使用data过滤器时,该机制将确保在大多数情况下(包括CVE-2007-4559)安全提取tar文件。
如果你有能力的话,在处理不受信任的tar文件时,应该使用Python版本>=3.11.4,以便利用提供的安全功能。只有在无法使用语言特性来实现此目的时,才应该实施被接受的答案。
祝你好运,疲惫的工程师同伴们...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接