Небезопасная десериализация
Разбираем, почему десериализация недоверенных данных открывает путь к выполнению чужого кода, и как закрыть этот класс уязвимостей.
Небезопасная десериализация — уязвимость, при которой приложение восстанавливает объект из присланных извне байтов и в процессе восстановления выполняет логику, заложенную атакующим.
Сериализация — это превращение объекта в поток байтов (для хранения или передачи), а десериализация — обратная операция. Проблема возникает, когда программа доверяет источнику этих байтов. Если поток пришёл от пользователя — из cookie, тела запроса, очереди сообщений, кеша — то фактически пользователь управляет тем, какие объекты будут созданы в памяти сервера. В худшем случае это приводит к выполнению кода на сервере (класс уязвимостей RCE — Remote Code Execution). В OWASP Top 10 2021 это входит в категорию A08: Software and Data Integrity Failures.
Изучаем мы это исключительно для защиты собственного кода. Подбор «волшебной» строки, которая выполнит команду на чужом сервере, — это несанкционированное воздействие (статьи 272 и 273 УК РФ). Наша цель — научиться писать код, в котором такая атака невозможна в принципе, и распознавать опасные паттерны при ревью.
Зачем это знать защитнику
Десериализация недоверенных данных — тихая уязвимость: внешне функция «просто читает объект», и код проходит ревью, потому что выглядит безобидно. Между тем именно из-за неё происходили громкие компрометации Java- и PHP-приложений. Разработчик, который понимает механику, видит опасную строку сразу и не ставит десериализатор на путь недоверенных данных. Инженер Blue Team понимает, какие форматы и какие точки входа надо контролировать в первую очередь.
Как возникает уязвимость
Корень проблемы — в том, что некоторые форматы сериализации сохраняют не только данные, но и тип объекта, а восстановление объекта запускает методы класса. То есть в потоке байтов закодировано «создай объект класса X», и при десериализации этот класс начинает работать. Если в приложении (или в его зависимостях) есть класс, который при создании/уничтожении выполняет что-то чувствительное — запуск процесса, чтение файла, сетевой вызов, — атакующий выбирает именно такой класс.
Опасный паттерн в Python выглядит так — pickle на данных из запроса:
# УЯЗВИМО: pickle восстанавливает ПРОИЗВОЛЬНЫЙ объект из недоверенных байтов
import pickle
def load_session(raw_bytes):
# raw_bytes пришли из cookie / тела запроса = недоверенный источник
return pickle.loads(raw_bytes) # опасно: формат несёт инструкции, а не только данные
Документация Python прямо предупреждает: модуль pickle небезопасен — никогда не распаковывайте данные из недоверенного источника. Аналогичные риски есть у нативной сериализации в других экосистемах (Java ObjectInputStream, PHP unserialize(), .NET BinaryFormatter, небезопасные YAML-загрузчики). Везде причина одна: формат позволяет закодировать тип и поведение, а не только значения.
Как это работает под капотом
Представим формат, который кодирует «вызови функцию F с аргументом A, чтобы восстановить объект». Десериализатор честно читает эту инструкцию и выполняет её — для него это штатный механизм восстановления состояния. Атакующему не нужно «ломать» парсер: он использует парсер по назначению, просто подавая объект, цепочка восстановления которого приводит к нужному действию. Такую цепочку из существующих в коде классов называют gadget chain. Ключевой вывод: уязвимость не в одном «плохом» классе, а в самом факте десериализации недоверенного ввода форматом, который умеет восстанавливать произвольные типы. Поэтому и защита строится не на «починке формата», а на отказе доверять источнику.
Как защититься
1. Не десериализуйте недоверенные данные опасными форматами
Главное правило: данные, пришедшие от пользователя, нельзя восстанавливать форматами, которые несут тип и поведение (pickle, нативная Java-сериализация, BinaryFormatter). Для обмена с внешним миром используйте форматы только для данных — JSON. JSON описывает значения (строки, числа, списки, словари) и не умеет кодировать «создай такой-то объект и вызови метод».
# БЕЗОПАСНО: JSON переносит только данные, без типов и поведения
import json
def load_session(raw_text):
data = json.loads(raw_text) # вернётся обычный dict/list/строки/числа
# дальше мы САМИ валидируем поля и собираем доверенный объект
user_id = int(data["user_id"]) # явная проверка типа
role = data["role"]
if role not in ("user", "editor", "admin"):
raise ValueError("недопустимая роль")
return {"user_id": user_id, "role": role}
Вывод:
load_session('{"user_id":"7","role":"editor"}') -> {'user_id': 7, 'role': 'editor'}
load_session('{"user_id":"7","role":"superuser"}') -> ValueError: недопустимая роль
2. Проверяйте целостность данных
Если объект всё же надо вернуть пользователю и принять обратно (например, токен сессии), подписывайте его. Сервер ставит криптографическую подпись (HMAC) при выдаче и проверяет её при приёме. Так клиент не сможет подменить содержимое — изменённые данные не пройдут проверку подписи, и до десериализации дело не дойдёт.
3. Allow-list классов
Когда без типизированного формата не обойтись, ограничьте, какие классы вообще разрешено восстанавливать, белым списком. Всё, чего нет в списке, — отклоняется. Это превращает «можно восстановить что угодно» в «можно восстановить только заранее одобренные безопасные типы» и обрубает gadget-цепочки.
# Принцип allow-list (псевдокод политики десериализации)
РАЗРЕШЕНО восстанавливать: { OrderDTO, AddressDTO, MoneyDTO }
ВСЁ ОСТАЛЬНОЕ -> отклонить и записать в журнал безопасности
4. Снижайте поверхность атаки
Принимайте на десериализацию минимум данных, держите зависимости обновлёнными (gadget-цепочки часто живут в старых версиях библиотек) и логируйте отклонённые попытки восстановления неизвестных типов — это сигнал для мониторинга.
Итоги
- Десериализация недоверенных данных опасна, потому что формат может нести не только значения, но и тип и поведение объекта.
- Никогда не распаковывайте пользовательский ввод через
pickle, нативную Java-сериализацию,BinaryFormatterи подобные. - Для внешнего обмена используйте форматы только для данных (JSON) и валидируйте каждое поле вручную.
- Где нужен типизированный формат — подпись целостности (HMAC) и строгий allow-list классов.
- Несанкционированное воздействие на чужие системы наказуемо (УК РФ ст. 272/273); тренируемся только на своём коде и в разрешённой лаборатории.