Подтвердить что ты не робот

Безопасное извлечение zip или tar с помощью Python

Я пытаюсь извлечь загруженные пользователем файлы 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 в этом случае? Если да, то каким образом я могу гарантировать, что файлы никогда не выйдут за пределы целевого каталога?

4b9b3361

Ответ 1

Примечание.. Начиная с python 2.7.4, это не проблема для ZIP-архивов. Подробности в нижней части ответа. Этот ответ посвящен архивам tar.

Чтобы выяснить, на что указывает путь, используйте os.path.abspath() (но обратите внимание на оговорку о символических ссылках в качестве компонентов пути). Если вы нормализуете путь из своего zip файла с помощью abspath и он не содержит текущий каталог в качестве префикса, он указывает на него.

Но вам также нужно проверить значение любой символической ссылки, извлеченной из вашего архива (оба файла 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-архивов: метод zipfile.extract() запрещает создание файлов вне песочницы:

Примечание. Если имя элемента-члена является абсолютным путем, разделительная точка диска /UNC и ведущая (обратная) слэши будут удалены, например: ///foo/bar становится foo/bar в Unix и C:\foo\bar становится foo\bar в Windows. И все компоненты ".." в имени элемента-члена будут удалены, например: ../../foo../../ba..r станет foo../ba..r. В Windows недопустимые символы (:, <, >, |, ", ? и *)) заменены символом подчеркивания (_).

Класс tarfile не был подобным образом дезинфицирован, поэтому приведенный выше ответ все еще сохраняется.

Ответ 2

Используйте ZipFile.infolist()/TarFile.next()/TarFile.getmembers(), чтобы получить информацию о каждой записи в архиве, нормализовать путь, открыть файл самостоятельно, использовать ZipFile.open()/TarFile.extractfile(), чтобы получить файл-как для запись и скопировать данные записи самостоятельно.

Ответ 3

Скопируйте zip файл в пустой каталог. Затем используйте os.chroot, чтобы сделать этот каталог корневым каталогом. Затем разархивируйте там.

В качестве альтернативы вы можете вызвать unzip самостоятельно с флагом -j, который игнорирует каталоги:

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

Ответ 4

Вопреки популярному ответу, безопасные распакованные файлы не полностью решены с Python 2.7.4. Экстракционный метод по-прежнему опасен и может привести к обходу пути, либо непосредственно, либо путем разворачивания символических ссылок. Вот мое окончательное решение, которое должно предотвращать обе атаки во всех версиях Python, даже версий до Python 2.7.4, где метод extract был уязвимым:

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)

Отредактировано: Фиксированное имя имени переменной. Спасибо Юусо Охотену.