Безопасная распаковка ZIP в iOS: пути и лимиты

Безопасная распаковка ZIP в iOS: пути и лимиты

Безопасная распаковка ZIP в iOS: пути и лимиты

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

Сначала определите границы импорта

В рамках этой политики принимаются только обычные файлы и каталоги; символические ссылки и неизвестные типы отклоняются. Для каждого импорта создавайте отдельный промежуточный каталог и переносите данные в рабочий каталог приложения только после полной проверки. Если хотя бы одна запись не прошла проверку, удаляйте все промежуточные данные, не сохраняя «уже успешно импортированную часть».

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

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

Создайте исполняемую проверку путей

Сохраните приведённый ниже код в файл zip-path-check.swift и выполните на облачном Mac команду xcrun swift zip-path-check.swift. Скрипт проверяет только имена записей и не читает ZIP. В проекте имена и типы записей, полученные от библиотеки распаковки, должны проходить через ту же политику. Успешное выполнение команды не означает, что весь процесс импорта безопасен.

import Foundation

enum PathError: Error {
    case invalid
}

func safeComponents(_ name: String) throws -> [String] {
    guard !name.isEmpty,
          !name.hasPrefix("/"),
          !name.contains("\\"),
          !name.contains(":"),
          !name.unicodeScalars.contains(where: {
              CharacterSet.controlCharacters.contains($0)
          }) else {
        throw PathError.invalid
    }

    let body = name.hasSuffix("/") ? String(name.dropLast()) : name
    let parts = body.split(
        separator: "/",
        omittingEmptySubsequences: false
    ).map(String.init)

    guard !parts.isEmpty,
          parts.allSatisfy({
              !$0.isEmpty && $0 != "." && $0 != ".."
          }) else {
        throw PathError.invalid
    }
    return parts
}

let cases: [(String, Bool)] = [
    ("docs/readme.txt", true),
    ("docs/", true),
    ("report..txt", true),
    ("../outside.txt", false),
    ("docs/../../outside.txt", false),
    ("/tmp/outside.txt", false),
    ("docs//readme.txt", false),
    ("docs/./readme.txt", false),
    ("docs\\..\\outside.txt", false),
    ("C:/outside.txt", false),
    ("docs/\u{0000}bad.txt", false)
]

for (name, expected) in cases {
    let accepted = (try? safeComponents(name)) != nil
    precondition(accepted == expected, "Path policy mismatch")
}
print("Path checks passed")

Это намеренно строгая политика импорта: допустимы только относительные пути с разделителем /, а у каталога разрешён один завершающий слеш. Двоеточия и управляющие символы также запрещены, чтобы избежать различий в интерпретации на разных файловых системах. Если такие символы действительно нужны для работы приложения, добавьте явные тестовые примеры, а не ослабляйте все проверки сразу.

Проверьте ссылки и конфликты путей назначения

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

Создавайте индекс каждого пути назначения по его компонентам. Помимо точных дубликатов, блокируйте конфликты между родительским и дочерним путями: например, когда сначала записывается файл docs, а затем — docs/a.txt. При обратном порядке записей импорт тоже должен завершаться отказом, иначе результат будет зависеть от порядка элементов в архиве.

Для имён, различающихся регистром, и эквивалентных имён Unicode нужна отдельная политика. Если импортируемые данные не требуют различать такие имена, для обнаружения конфликтов можно применять к компонентам каноническую нормализацию с композицией и свёртку регистра, сохраняя исходные имена при записи. Не делайте выводов о поведении записи в iOS на основании чувствительности текущего тома облачного Mac к регистру. Подготовьте как минимум два набора образцов — с коллизиями регистра и с комбинируемыми символами — и повторите проверку в целевом симуляторе.

Проверяйте лимиты по фактически записываемым данным

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

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

После сбоя записи проверяйте утверждениями, что промежуточный каталог удалён, рабочий каталог приложения не изменился, а содержимое заранее размещённого рядом с каталогом импорта контрольного файла осталось прежним. Это полезнее, чем просто проверить, что функция выбросила ошибку: ошибка могла возникнуть уже после записи файла за пределами разрешённого каталога.

Включите проверки в регрессионный контроль и сохраните границы изоляции

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

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

В завершение убедитесь в трёх вещах: неудачный импорт не оставляет частичных данных, успешный создаёт только ожидаемые файлы, а лимиты контролируются по фактическому числу байтов. Функция проверки пути — лишь первый рубеж; окончательный критерий приёмки — проверки результатов записи при распаковке реальных архивов.

Часто задаваемые вопросы

Достаточно запретить имена с двумя точками?

Нет. Запрещайте отдельный компонент .., абсолютные пути и неподдерживаемые разделители. Проверяйте также ссылки, повторные цели и границы каталога при записи. Имя report..txt само по себе допустимо.

Можно доверять размеру из каталога ZIP?

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

Передайте очередь сборки выделенному облачному Mac

Выберите физический узел, модель Mac и период оплаты. Запускайте сборки Xcode и задачи автоматизации в собственной среде macOS. Актуальный статус доступности отображается в консоли.

Продолжите проверку среды сборки

Сверьте характеристики модели и инструкции, чтобы определить следующие шаги настройки.