🐈

shutil.unpack_archiveで展開されないファイル名

2022/08/06に公開

はじめに

pythonのshutil.unpack_archive関数はお手軽にzipを展開してくれます。
しかしながら、展開結果が常に正しいとは限りません。
例えば、以下のようなファイル名に「..」を含むファイルの展開がされていません。

testzip_content
└── hoge
    ├── testOK.txt
    └── testNG..txt

原因

これは、shutil.unpack_archive中のzipの展開処理を確認すれば簡単にわかります。

https://github.com/python/cpython/blob/main/Lib/shutil.py#L1204-L1218

上記のコメント通り、zip中のファイルのパスが「/」で始まったり「..」が含まれているものについては展開を行いません。
おそらくは、信頼できないZIPファイルの展開中に指定したフォルダ外への書き込みを防ぐための処置だと推測できますが、これによりファイル名に「..」が含まれるものは展開できなくなってしまいます。

対策

おそらく実装の意図としては、「hoge/../../../xxxx.txt」などのパスの展開を予防するものと推測できるので、以下のような実装に直せば対応可能です。

import zipfile
import os

def _unpack_zipfile(filename, extract_dir):
    """Unpack zip `filename` to `extract_dir`
    """
    import zipfile  # late import for breaking circular dependency

    if not zipfile.is_zipfile(filename):
        raise ReadError("%s is not a zip file" % filename)

    zip = zipfile.ZipFile(filename)
    try:
        for info in zip.infolist():
            name = info.filename

            # don't extract absolute paths or ones with .. in them
            path_list = name.split('/') # todo window path separeter
            if name.startswith('/') or '..' in path_list:
                continue

            targetpath = os.path.join(extract_dir, *name.split('/'))
            if not targetpath:
                continue

            _ensure_directory(targetpath)
            if not name.endswith('/'):
                # file
                with zip.open(name, 'r') as source, \
                        open(targetpath, 'wb') as target:
                    copyfileobj(source, target)
    finally:
        zip.close()


def _ensure_directory(path):
    """Ensure that the parent directory of `path` exists"""
    dirname = os.path.dirname(path)
    if not os.path.isdir(dirname):
        os.makedirs(dirname)

COPY_BUFSIZE = 1024 * 1024

def copyfileobj(fsrc, fdst, length=0):
    """copy data from file-like object fsrc to file-like object fdst"""
    if not length:
        length = COPY_BUFSIZE
    # Localize variable access to minimize overhead.
    fsrc_read = fsrc.read
    fdst_write = fdst.write
    while True:
        buf = fsrc_read(length)
        if not buf:
            break
        fdst_write(buf)

_unpack_zipfile('test.zip', 'testzip_content')

Discussion