使用Python安全地提取zip或tar

时间:2012-04-08 03:07:47

标签: python zip tar zipfile tarfile

我正在尝试将用户提交的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之外结束?如果是这样,我可以通过哪种方式确保文件永远不会出现在目标目录之外?

4 个答案:

答案 0 :(得分:38)

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

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

但您还需要检查从存档中提取的任何符号链接的(tarfiles和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: 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变为foo/bar Unix,C:\foo\bar在Windows上变为foo\bar。并且会删除成员文件名中的所有".."个组件,例如:../../foo../../ba..r变为foo../ba..r。在Windows上,非法字符(:<>|"?*)[被下划线(_)取代。

tarfile课程没有经过同样的消毒,因此上述答案仍然适用。

答案 1 :(得分:3)

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

答案 2 :(得分:2)

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

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

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

答案 3 :(得分:2)

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

import zipfile, os

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

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