使用Python安全地提取zip或tar

使用Python安全地提取zip或tar,python,zip,tar,zipfile,tarfile,Python,Zip,Tar,Zipfile,Tarfile,我正在尝试将用户提交的zip和tar文件解压缩到一个目录中。zipfile方法的文档(与tarfile的方法类似)指出,路径可能是绝对路径或包含超出目标路径的。路径。相反,我可以自己使用extract,如下所示: some_path = '/destination/path' some_zip = '/some/file.zip' zipf = zipfile.ZipFile(some_zip, mode='r') for subfile in zipf.namelist(): zipf

我正在尝试将用户提交的zip和tar文件解压缩到一个目录中。zipfile方法的文档(与tarfile的方法类似)指出,路径可能是绝对路径或包含超出目标路径的
路径。相反,我可以自己使用
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)

这安全吗?在这种情况下,存档中的文件是否可能在
某些路径之外结束?如果是这样,我可以用什么方法确保文件不会出现在目标目录之外?

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

注意:从python 2.7.4开始,这对于ZIP归档来说是不成问题的。答案底部的细节。这个答案主要关注tar档案

要找出路径真正指向的位置,请使用
os.path.abspath()
(但请注意符号链接作为路径组件的注意事项)。如果使用
abspath
规范化zipfile中的路径,并且该路径不包含当前目录作为前缀,则它指向它的外部

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

这就是前面提到的警告:
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存档来说不是问题:该方法禁止在沙箱之外创建文件:

注意:如果成员文件名是绝对路径,则驱动器/UNC sharepoint和前导(后)斜杠将被去除,例如:
///foo/bar
在Unix上变成
foo/bar
,在Windows上变成
C:\foo\bar
。成员文件名中的所有组件都将被删除,例如:
。/../foo../../ba..r
将变成
foo../ba..r
。在Windows上,非法字符(
*
)被替换为下划线(\ux)


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

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

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

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

与流行的答案相反,从Python 2.7.4开始,安全地解压缩文件并没有得到完全解决。extractall方法仍然很危险,可以直接或通过解压缩符号链接导致路径遍历。这是我的最终解决方案,它应该可以在所有版本的Python中防止这两种攻击,即使是之前的版本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
。感谢Lizzard

这看起来很难确保我的操作正确-尤其是如果你有像
。/../../../../../../../../../../something/file.txt这样的文件-目标应该在哪里?以前没有人提供代码来处理这个问题?没有人可以为你回答,因为只有您了解您的应用程序需求。我不同意。其他工具会自动为您执行此操作-例如,
tar
命令会自动删除绝对路径,除非您指定
--绝对名称
。任何委托给
tar
的软件都必须遵守此操作。这是您的软件。请在您遇到一个具有无效/不允许路径的条目您有3个选项:1)无论如何尝试提取,并捕获任何错误2)提取到修改的路径3)不提取。我无法告诉您哪个策略适合您的应用程序。您可以假设新的沙盒目录为空,但您仍然需要注意e我概述的漏洞利用:首先存档包含指向另一个目录的符号链接,然后是使用符号链接作为其路径的文件。realpath会将提取的文件转换为其真实路径,因此您可能只需在提取后进行检查?对,您可以在提取后立即使用
realpath
测试每个符号链接(这意味着您不能使用
extractall
解压归档文件,因为您需要在解压每个文件后进行检查)。根据自述文件,
archive.extract()
如果检测到超出范围的文件,将引发异常。异常将终止批量解压,并且无法恢复。甚至似乎没有