Анализ вложенных архивов с ограничением глубины: как безопасно распаковывать архивы в архивах

Архив внутри архива — частая ситуация при обработке загружаемых файлов, почтовых вложений и данных из внешних источников. Наивная рекурсивная распаковка «пока есть архивы — распаковывай» приводит к двум проблемам: исчерпанию диска и памяти из-за вредоносных или просто повреждённых файлов, а также зависанию обработки на бесконечной вложенности. Решение обеих задач — ограничение глубины рекурсии вместе с контролем суммарного объёма распакованных данных.

Главный принцип: глубина вложенности — не единственный лимит. Работоспособная система анализа ограничивает одновременно четыре параметра: уровень вложенности, суммарный размер распакованных данных, количество файлов и время обработки. Ниже разобрано, как это устроено, какие значения выбирать и как реализовать проверку на практике.

Зачем вообще ограничивать глубину

Рекурсивная обработка без ограничений уязвима к так называемым zip-бомбам — архивам, которые при распаковке разворачиваются в гигабайты или терабайты данных. Классический пример — многослойные архивы, где каждый слой сжимает предыдущий ещё сильнее: итоговый коэффициент расширения у таких конструкций достигает миллионов раз. Отдельная категория угроз — «квадратичные» архивы, которые специально сконструированы так, что один и тот же фрагмент данных ссылается сам на себя, и распаковщик тратит ресурсы непропорционально размеру файла.

Даже без злого умысла глубокая вложенность возникает естественным образом: пользователь упаковывает папку проекта, внутри которой лежит скачанный ранее архив с библиотекой, внутри него — ещё один архив с примерами. Пять-семь уровней в легитимных данных встречаются редко, но встречаются. Поэтому жёсткий лимит вроде «не больше трёх уровней» ломает часть нормальных сценариев, а его отсутствие открывает дверь к исчерпанию ресурсов.

Что происходит при отсутствии лимитов

  • Переполнение диска. Распакованные данные занимают место, обработка падает посреди работы, временные файлы остаются на диске и продолжают занимать место после сбоя.
  • Исчерпание памяти. Если содержимое читается целиком в память (типичная ошибка при работе с потоками), процесс завершается по Out of Memory.
  • Бесконечная рекурсия. Повреждённый или специально собранный архив может ссылаться сам на себя; без счётчика глубины обработка никогда не завершится.
  • Отказ в обслуживании. Один загруженный файл способен заблокировать весь конвейер обработки: очередь растёт, остальные задачи ждут.

Четыре лимита вместо одного

Ограничение только глубины недостаточно, потому что даже двухуровневый архив может развернуться в огромный объём данных. И наоборот: неглубокий, но широкий архив с тысячами мелких файлов создаст нагрузку на файловую систему. Поэтому практичный набор ограничений выглядит так:

Лимит От чего защищает Типичный порядок значения
Глубина вложенности Бесконечная рекурсия, многослойные бомбы 3–10 уровней в зависимости от сценария
Суммарный распакованный объём Zip-бомбы любого типа Доли от доступного места, например сотни МБ — единицы ГБ
Количество файлов Архивы с миллионами записей Сотни — десятки тысяч
Время обработки Медленные форматы, деградация производительности Секунды — минуты на один исходный файл

Конкретные числа зависят от вашего сценария. Для антивирусной проверки почтовых вложений глубина 5–7 и суммарный объём порядка нескольких сотен мегабайт обычно достаточны. Для внутреннего конвейера, обрабатывающего доверенные выгрузки, лимиты могут быть мягче. Важно другое: значения должны быть конфигурируемыми, а не зашитыми в код, потому что требования меняются быстрее, чем успевают обновляться программы.

Проверка до распаковки, а не после

Ключевой момент реализации: лимиты должны проверяться по метаданным архива до извлечения содержимого. Почти все распространённые форматы хранят в заголовке список записей с несжатыми размерами. Это позволяет заранее оценить:

  • заявленный суммарный размер всех файлов внутри;
  • количество записей;
  • наличие вложенных архивов по расширениям имён.

Если заявленный суммарный размер превышает лимит — обработку можно остановить сразу, не распаковывая ни байта. Оговорка: заголовкам нельзя слепо доверять. Размеры в метаданных могут быть искажены намеренно или повреждены, поэтому фактический объём всё равно нужно считать в процессе распаковки. Метаданная проверка — быстрый фильтр первого уровня, а поточный подсчёт — гарантия второго уровня.

Алгоритм обхода с контролем глубины

Логика обработки сводится к обходу дерева: корневой архив — узел первого уровня, каждый архив внутри него — следующий уровень. Проще всего описать её как последовательность шагов:

  1. Принять входной файл и зарегистрировать для него контекст: текущая глубина (0), накопленный объём (0), счётчик файлов (0), дедлайн по времени.
  2. Определить тип файла по сигнатуре содержимого, а не только по расширению: переименованный исполняемый файл с расширением .zip — обычная маскировка.
  3. Прочитать оглавление архива. Проверить количество записей и заявленный суммарный размер против лимитов. При превышении — остановить обработку и пометить файл как подозрительный.
  4. Распаковывать записи по одной в потоковом режиме. После каждой записи увеличивать счётчики фактически записанных байт и файлов, сверять их с лимитами. При превышении — немедленно прервать распаковку и удалить уже извлечённое.
  5. Для каждой извлечённой записи проверить, является ли она архивом (по сигнатуре). Если да и текущая глубина плюс один не превышает лимита — поставить её в очередь на обработку с увеличенной глубиной. Если лимит глубины исчерпан — зафиксировать факт вложенного архива и решить по политике: пропустить, пометить или передать в ручную проверку.
  6. Повторять, пока очередь не опустеет или не сработает дедлайн по времени.
  7. По завершении удалить все временные файлы, включая те, что остались после прерывания по ошибке.

Обратите внимание на структуру «очередь вместо рекурсии». Рекурсивный вызов функции для каждого вложенного архива работает, но при большой глубине рискует упереться в стек вызовов, а главное — усложняет учёт общих лимитов. Явная очередь задач с передачей контекста (глубина, родительский идентификатор) делает обработку управляемой и позволяет вести дерево результатов: какой архив где был найден и что внутри него обнаружилось.

Учёт общего бюджета объёма

Суммарный лимит должен быть общим для всей ветки обработки, а не отдельным на каждый архив. Иначе злоумышленник упакует десять архивов по 90% лимита каждый и получит десятикратное превышение. На практике это означает общий счётчик на всю сессию обработки исходного файла, который передаётся вниз по дереву вложенности и инкрементируется каждой распаковкой независимо от уровня.

Выбор значений лимитов под сценарий

Единственно правильных чисел нет — они определяются балансом между полнотой анализа и устойчивостью системы. Ориентиры по типовым ситуациям:

  • Проверка файлов из недоверенных источников (вложения, загрузки пользователей): строгие лимиты — глубина 5–7, суммарный объём в пределах сотен мегабайт, жёсткий таймаут. Всё сверх лимита помечается как требующее ручного разбора, а не молча пропускается.
  • Обработка внутренних выгрузок и резервных копий: глубина может доходить до 10–15, объём — до единиц гигабайт, но обязательно с мониторингом фактических распределений: если реальные данные укладываются в 4 уровня, лимит 15 ничего не даёт, кроме ложного чувства защищённости.
  • Массовый анализ коллекций файлов: ключевой ресурс — время. Таймаут на один файл важнее остальных лимитов, иначе одна тяжёлая выборка остановит весь прогон.

Полезная практика — сначала измерить реальное распределение глубины и размеров на ваших данных, затем установить лимиты с запасом в несколько раз над наблюдаемым максимумом. Лимит, который регулярно срабатывает на легитимных файлах, либо слишком строгий, либо указывает на проблему в источнике данных — оба случая требуют внимания, а не отключения проверки.

Политика при превышении лимитов

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

  • Отказ с удалением временных данных. Базовая реакция для недоверенных источников: обработка прекращается, извлечённое удаляется, файл помечается как отклонённый с указанием причины.
  • Частичный результат. Для аналитических задач полезнее обработать то, что успели, и зафиксировать точку остановки: «распаковано 3 из 5 уровней, дальше лимит глубины». Так вы сохраняете видимость, а не получаете бинарный ответ «годен/не годен».
  • Эскалация. Файл, превысивший лимиты, направляется в карантин или на ручную проверку. Особенно это оправдано, когда превышение сочетается с другими признаками: двойным расширением имени, исполняемыми файлами внутри, расхождением заявленного и фактического размера.

Чего делать точно не стоит — логировать срабатывание и продолжать обработку дальше. Молчаливое игнорирование лимита превращает всю защиту в декорацию.

Типичные ошибки реализации

  • Доверие расширению файла. Тип архива определяется по первым байтам содержимого (магическим числам), а не по имени. Архив, замаскированный под изображение, обязан распознаваться как архив — или осознанно пропускаться по политике, но не по ошибке.
  • Лимит глубины без лимита объёма. Закрывает бесконечную рекурсию, но оставляет открытой атаку через двухуровневую бомбу.
  • Загрузка целого архива в память. Даже небольшой по размеру файл может разворачиваться в гигабайты; чтение должно быть потоковым, запись — на диск с подсчётом байт.
  • Отдельный бюджет на каждый вложенный архив. Суммарный расход должен считаться по всему дереву, иначе лимиты складываются.
  • Отсутствие очистки при сбое. Прерванная распаковка обязана удалять временные файлы в блоке гарантированной очистки, иначе диск постепенно заполняется мусором от неудачных обработок.
  • Игнорирование символических ссылок и путей с переходами. Записи вида «../» в путях внутри архива (path traversal) позволяют записать файл вне целевой директории. Перед записью путь нужно нормализовать и убедиться, что он остаётся внутри выделенного каталога.
  • Фиксированные лимиты в коде. Значения, зашитые в программу, невозможно оперативно изменить при изменении профиля угроз или состава данных.

Особенности форматов

Не все архивы одинаково удобны для такого анализа. ZIP хранит центральный каталог с размерами, поэтому предварительная оценка дешева. TAR не сжимает сам по себе и обычно применяется поверх отдельного компрессора (gzip, bzip2, xz) — здесь важно ограничивать уже распакованный поток компрессора, который может быть сколь угодно большим при маленьком сжатом файле. RAR и 7z имеют собственные форматы метаданных; поддержка зависит от используемой библиотеки, и для них особенно актуален контроль фактического объёма при распаковке, поскольку заявленные размеры не всегда доступны или надёжны.

Ещё один нюанс — шифрованные вложенные архивы. Пароль неизвестен, содержимое недоступно, и это нормально: система должна зафиксировать наличие защищённого архива и его положение в дереве, а решение о дальнейших действиях (запросить пароль, отклонить, передать человеку) принимается по вашей политике. Попытки перебора паролей в автоматическом конвейере — отдельная и обычно нежелательная активность.

Наблюдаемость и журналирование

Анализ без журнала бесполезен при разборе инцидентов. Минимальный набор сведений о каждой обработке:

  • идентификатор исходного файла и его хеш;
  • дерево вложенности: путь к каждому найденному архиву и его уровень;
  • фактические суммарные объём и количество извлечённых файлов;
  • причина остановки, если лимит сработал, с указанием конкретного значения;
  • время обработки.

Такой журнал отвечает на вопросы, которые неизбежно возникнут: почему этот файл не прошёл проверку, что именно было внутри, не потеряли ли мы данные из-за слишком строгого лимита. Агрегированная статистика по журналам, в свою очередь, показывает, насколько выбранные лимиты соответствуют реальным данным, и подсказывает, когда их стоит пересмотреть.

Если условия такие — действуйте так

  • Вы принимаете файлы от внешних пользователей. Строгие лимиты по всем четырём параметрам, определение типа по сигнатуре, отказ с эскалацией при превышении, обязательная очистка временных каталогов.
  • Вы обрабатываете собственные резервные копии и выгрузки. Мягкие лимиты, настроенные по измеренному распределению, частичный результат вместо отказа, приоритет — не потерять данные из-за пограничного срабатывания.
  • Вы строите массовый анализ большого корпуса файлов. Жёсткий таймаут на файл, очередь задач вместо рекурсии, подробный журнал и статистика срабатываний для последующей настройки.
  • Вы получили файл, который подозреваете в том, что это бомба. Не распаковывайте его на рабочей машине обычными средствами. Анализируйте в изолированной среде с ограниченными ресурсами, начиная с чтения метаданных без распаковки.

С чего начать внедрение

Если система анализа уже существует, добавьте в неё четыре конфигурируемых лимита и общий счётчик объёма по дереву вложенности — это закрывает большинство практических рисков. Если система строится с нуля, начните с потоковой распаковки с подсчётом фактических байт и явной очередью задач: эти два элемента составляют каркас, на который потом навешиваются метаданные проверки, политики реагирования и журналирование. Затем измерьте реальные данные и откалибруйте значения — лимиты, установленные «из головы», почти всегда оказываются либо дырявыми, либо избыточно строгими.

И последнее: ограничение глубины — это защита доступности, а не полная безопасность. Оно предотвращает исчерпание ресурсов, но не заменяет проверку содержимого на вредоносность, контроль путей и работу с недоверенными форматами в изолированной среде. Комплекс из этих мер и даёт устойчивую обработку вложенных архивов.

PEFile.ru