Сообщить об ошибке.

Фильтры извлечения модуля tarfile в Python

Новое в Python 3.12

Формат tar предназначен для отображения всех деталей UNIX-подобной файловой системы, что делает его очень мощным. К сожалению, эти функции позволяют легко создавать tar–файлы, которые при извлечении имеют непреднамеренные и, возможно, вредоносные последствия. Например, извлечение файла tar может перезаписать произвольные файлы различными способами (например, с помощью абсолютных путей, компонентов .. path или символических ссылок, которые влияют на более поздние элементы).

В большинстве случаев полная функциональность не требуется. Таким образом, tarfile поддерживает фильтры извлечения: механизм ограничения функциональности и, таким образом, устранения некоторых проблем безопасности.

Фильтры извлечения были добавлены в Python 3.12, но могут быть перенесены в более старые версии в качестве обновлений безопасности. Чтобы проверить, доступна ли эта функция, вместо проверки версии Python используйте hasattr(tarfile, 'data_filter') .

Аргументом фильтра для TarFile.extract() или TarFile.extractall() может быть:

  • строка 'fully_trusted': соблюдает все метаданные, указанные в архиве. Следует использовать, если пользователь полностью доверяет архиву или реализует свою собственную комплексную проверку.

  • строка 'tar': учитывает большинство функций, специфичных для tar (т. е. функций UNIX-подобных файловых систем), но блокирует функции, которые с большой вероятностью могут оказаться неожиданными или вредоносными. Подробности смотрите в tarfile.tar_filter().

  • строка 'data': игнорировать или блокировать большинство функций, специфичных для UNIX-подобных файловых систем. Предназначен для извлечения кроссплатформенных архивов данных. Подробности смотрите в tarfile.data_filter().

  • None (по умолчанию): использует TarFile.extraction_filter.

    Если это значение также равно None (по умолчанию), то поднимается исключение DeprecationWarning и возвращается к фильтру 'fully_trusted', опасное поведение которого соответствует предыдущим версиям Python.

    В Python 3.14 по умолчанию будет использоваться фильтр 'data'. Можно переключиться и раньше; смотрите описание TarFile.extraction_filter.

  • Вызываемый объект, который будет вызываться для каждого извлеченного элемента с tarfile.TarInfo, описывающим элемент и путь назначения, куда извлекается архив (т. е. один и тот же путь используется для всех членов):

    filter(member: TarInfo, path: str, /) -> TarInfo | None
    

    Вызываемый объект вызывается непосредственно перед извлечением каждого члена, поэтому он может учитывать текущее состояние диска.

    Он может:

    • вернуть объект tarfile.TarInfo, который будет использоваться вместо метаданных в архиве, или
    • вернуть None, и в этом случае элемент будет пропущен, или
    • вызвать исключение, чтобы прервать операцию или пропустить элемент, в зависимости от уровня ошибки TarFile.errorlevel. Обратите внимание: когда извлечение прерывается, TarFile.extractall() может оставить архив частично извлеченным.

Пример использования фильтра извлечения

import tarfile

tar = tarfile.open("sample.tar.gz")
tar.extractall(filter='data')
tar.close()

# Эквивалентно

with tarfile.open("sample.tar.gz") as tar:
    tar.extractall(filter='data')

Именованные фильтры по умолчанию

Предопределенные именованные фильтры доступны как функции, поэтому их можно повторно использовать в пользовательских фильтрах:

tarfile.fully_trusted_filter(member, path):

Фильтр tarfile.fully_trusted_filter() возвращает элемент member без изменений. Это реализует фильтр 'fully_trusted'.

tarfile.tar_filter(member, path):

Фильтр tarfile.tar_filter() реализует фильтр 'tar'. Удаляет начальные косые черты (/ и os.sep) из имен файлов.

  • Отказывается извлекать файлы с абсолютными путями (в случае, если имя является абсолютным даже после удаления косых черт, например C:/foo в Windows). Это вызывает исключение AbsolutePathError.
  • Отказывается извлекать файлы, абсолютный путь к которым (после перехода по символическим ссылкам) окажется за пределами path. Это вызывает ошибку OutsideDestinationError.
  • Очищает биты (setuid, setgid, sticky) и групповые/другие биты записи (S_IWOTH).

Возвращает измененный элемент tarfile.TarInfo.

tarfile.data_filter(member, path):

Фильтр tarfile.data_filter() реализует фильтр 'data'. В дополнение к тому, что делает tarfile.tar_filter():

  • Отказывается извлекать ссылки (жесткие или программные), которые ссылаются на абсолютные пути, или те, которые ссылаются за пределы path. Это вызывает ошибку AbsoluteLinkError или LinkOutsideDestinationError.

    Обратите внимание, что такие файлы отклоняются даже на платформах, которые не поддерживают символьные ссылки.

  • Отказывается извлекать файлы устройств (включая каналы). Это вызывает SpecialFileError.

  • Для обычных файлов, включая жесткие ссылки:

    • Устанавливает права владельца на чтение и запись (S_IWUSR).
    • Удаляет разрешение для группы и других исполняемых файлов (S_IXOTH), если у владельца его нет (S_IXUSR).
  • Для других файлов (каталогов) устанавливает значение mode=None, чтобы методы извлечения не применяли биты разрешений.

  • Устанавливает для информации о пользователе и группе (uid, gid, uname, gname) значение None, чтобы методы извлечения их не устанавливали.

Возвращает измененный элемент tarfile.TarInfo.

Ошибки фильтра извлечения

Когда фильтр отказывается извлечь файл, он выдает соответствующее исключение - подкласс FilterError.

Если TarFile.errorlevel равен 1 или более, то это прервет извлечение файлов из архива. При errorlevel=0 ошибка будет зарегистрирована и элемент будет пропущен, но извлечение продолжится.

Советы предварительной проверки при извлечении архива

Даже фильтр filter='data' не подходит для извлечения ненадежных файлов без предварительной проверки. Помимо прочего, предопределенные фильтры не предотвращают атаки типа "отказ в обслуживании". Пользователям следует выполнить дополнительные проверки.

Вот неполный список вещей, на которые стоит обратить внимание:

  • Извлеките архив в новый временный каталог, чтобы предотвратить, например, использования уже существующих ссылок и упростить очистку файловой системы после неудачного извлечения.
  • При работе с ненадежными данными используйте внешние (например, на уровне ОС) ограничения на использование диска, памяти и ЦП.
  • Проверьте имена файлов на соответствие списку разрешенных символов (чтобы отфильтровать управляющие символы, сбивающие с толку символы, внешние разделители путей и т.д.).
  • Убедитесь, что имена файлов имеют ожидаемые расширения (не рекомендуется использовать файлы, которые выполняются при "нажатии на них", или файлы без расширений, такие как имена специальных устройств Windows).
  • Ограничьте количество извлеченных файлов, общий размер извлеченных данных, длину имени файла (включая длину символической ссылки) и размер отдельных файлов.
  • Проверьте наличие файлов, которые будут дублироваться в файловых системах, нечувствительных к регистру.

Также обратите внимание, что:

  • Файлы tar могут содержать несколько версий одного и того же файла. Ожидается, что более поздние записи перезапишут более ранние. Эта функция имеет решающее значение для обновления ленточных архивов, но ею можно злоупотреблять.
  • Модуль tarfile не защищает от проблем с "живыми" данными, например. злоумышленник возится с каталогом назначения (или исходным каталогом) во время извлечения (или архивирования).

Поддержка старых версий Python

В следующих примерах показано, как поддерживать версии Python с этой функцией и без нее. Обратите внимание, что установка extraction_filter повлияет на любые последующие операции.

Полностью доверенный архив:

my_tarfile.extraction_filter = (lambda member, path: member)
my_tarfile.extractall()

Используем фильтр 'data', если он доступен, если нет, то возвращаемся к поведению Python 3.11 ('fully_trusted'):

my_tarfile.extraction_filter = getattr(tarfile, 'data_filter',
                                       (lambda member, path: member))
my_tarfile.extractall()

Используем фильтр 'data' и завершаем работу с ошибкой, если он недоступен:

my_tarfile.extractall(filter=tarfile.data_filter)

# или

my_tarfile.extraction_filter = tarfile.data_filter
my_tarfile.extractall()

Используем фильтр 'data' и предупреждаем, если он недоступен:

if hasattr(tarfile, 'data_filter'):
    my_tarfile.extractall(filter='data')
else:
    # remove this when no longer needed
    warn_the_user('Extracting may be unsafe; consider updating Python')
    my_tarfile.extractall()

Пример пользовательского фильтра извлечения с отслеживанием состояния

Хотя методы извлечения tar-файла используют простой вызываемый фильтр, пользовательские фильтры могут представлять собой более сложные объекты с внутренним состоянием. Их полезно писать как менеджеры контекста, чтобы потом использовать следующим образом:

with StatefulFilter() as filter_func:
    tar.extractall(path, filter=filter_func)

Такой фильтр можно написать как-то так:

class StatefulFilter:
    def __init__(self):
        self.file_count = 0

    def __enter__(self):
        return self

    def __call__(self, member, path):
        self.file_count += 1
        return member

    def __exit__(self, *exc_info):
        print(f'{self.file_count} files extracted')