🐈
shutil.unpack_archiveで展開されないファイル名
はじめに
pythonのshutil.unpack_archive関数はお手軽にzipを展開してくれます。
しかしながら、展開結果が常に正しいとは限りません。
例えば、以下のようなファイル名に「..」を含むファイルの展開がされていません。
testzip_content
└── hoge
├── testOK.txt
└── testNG..txt
原因
これは、shutil.unpack_archive中のzipの展開処理を確認すれば簡単にわかります。
上記のコメント通り、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