【问题标题】:Safely extract zip or tar using Python使用 Python 安全地提取 zip 或 tar
【发布时间】:2012-04-21 01:12:40
【问题描述】:

我正在尝试将用户提交的 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 存档的漏洞仍然存在。
  • 很高兴知道,谢谢!

标签: python zip tar zipfile tarfile


【解决方案1】:

注意:从 python 2.7.4 开始,这对于 ZIP 存档不存在问题。答案底部的详细信息。此答案侧重于 tar 档案。

要找出路径真正指向的位置,请使用os.path.abspath()(但请注意有关将符号链接作为路径组件的警告)。如果您使用 abspath 规范化 zipfile 中的路径,并且它确实 not 包含当前目录作为前缀,则它指向它之外。

但您还需要检查从存档中提取的任何符号链接的 (tarfile 和 unix zipfile 都可以存储符号链接)。如果您担心众所周知的“恶意用户”会故意绕过您的安全性,而不是简单地将自身安装在系统库中的应用程序,这一点很重要。

这就是前面提到的警告:如果您的沙箱已经包含指向目录的符号链接,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: Hard link to", finfo.linkname
        elif finfo.islnk() and badlink(finfo,base):
            print >>stderr, finfo.name, "is blocked: Symlink 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 共享点和前导(反)斜杠将被剥离,例如:///foo/bar 在 Unix 上变为 foo/bar,而 @ 987654334@ 在 Windows 上变为 foo\bar。并且成员文件名中的所有".." 组件都将被删除,例如:../../foo../../ba..r 变为foo../ba..r。在 Windows 上,非法字符(:<>|"?*)[被] 替换为下划线 (_)。

tarfile 类还没有经过类似的清理,所以上述答案仍然适用。

【讨论】:

  • 你可以假设新的沙箱目录是空的
  • 我也这么想;但是您仍然需要注意我概述的漏洞利用:首先存档包含指向另一个目录的符号链接,然后是使用符号链接作为其路径的文件。
  • realpath 会将提取的文件转换为其真实路径,因此您可以在提取后检查它吗?
  • 对,你可以在解压后立即使用realpath测试每个符号链接(这意味着你不能使用extractall解压压缩包,因为你需要在解压每个文件后检查) .
  • 根据自述文件,Archive.extract() 将在检测到越界文件时引发异常。该异常将终止批量提取,并且无法恢复它。似乎甚至没有办法列出存档内容并一次提取一个文件。让我不为所动。
【解决方案2】:

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

【讨论】:

  • 这似乎很难确保我做对了——尤其是如果你有像../../../../subdir/../../something/file.txt 这样的文件——目的地应该在哪里?以前没有人提供过代码来处理这个问题吗?
  • 没有人可以为您解答,因为只有您了解您的应用需求。
  • 我不同意。其他工具会自动为您执行此操作 - 例如,tar 命令会自动删除绝对路径,除非您指定 --absolute-names
  • 任何委托给tar的软件都必须遵守这一点。这是您的软件。
  • sigh 当您遇到具有无效/不允许路径的条目时,您有 3 个选项:1) 仍然尝试提取,并捕获任何错误 2) 提取到修改后的路径3)不要提取。 我无法告诉您哪种政策适合您的申请。
【解决方案3】:

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

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

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

【讨论】:

  • 子进程模块适用于每个运行 Python、AFAICT 的平台。但是,如果您谈论的是 MS Windows,则有几个可用于处理 zip 文件的程序,例如 INFO-zip。特定的命令行当然需要根据您希望使用的程序进行调整。
  • 你说得对,os.chroot 是特定于 UNIX 的。但是,如果您搜索它们,您会发现类似 chroot 的 Windows 应用程序。当然,在这种情况下,真正的矫枉过正的解决方案是在虚拟机中运行unzip。 :-)
  • 这是一个非常简单的想法,但是 (a) 它只适用于 Unix 系统,并且 (b) 在 Unix 上,只有超级用户可以 chroot。在处理潜在不安全数据的过程中提升权限确实是错误的方法......
  • 使用 info-zip 的 unzip-j 标志作为 chroot 的替代,应该适用于任何解压缩工作的平台。
【解决方案4】:

与流行的答案相反,从 Python 2.7.4 开始,安全解压缩文件并没有完全解决。 extractall 方法仍然很危险,并且可能直接或通过解压缩符号链接导致路径遍历。这是我的最终解决方案,它应该可以防止所有版本的 Python 中的两种攻击,甚至是 Python 2.7.4 之前的版本,其中 extract 方法很容易受到攻击:

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

【讨论】:

  • 避免使用zipfile 作为参数名称,因为它与导入名称:AttributeError: 'str' object has no attribute 'ZipFile' 冲突。修复是将zipfile 参数重命名为例如zip_file.
  • 感谢您的评论。我修复了示例代码。我最初将它从我的项目中取出并编辑为独立的,显然忘记了对其进行测试。
  • 你为什么使用os.path.abspath而不是os.path.realpath?使用os.path.realpath不是更安全吗?
  • 好点。我将更新答案以反映该建议。 realpath 显然调用了abspath,所以realpath 应该就足够了。
猜你喜欢
  • 1970-01-01
  • 2010-12-24
  • 2017-12-16
  • 1970-01-01
  • 1970-01-01
  • 2018-11-15
  • 1970-01-01
相关资源
最近更新 更多