Перейти к содержимому
>_ITDITDПлатформа веб-безопасности

Руководства по безопасности

Небезопасная десериализация: чем опасно, когда внешние данные выбирают тип объекта, и как это предотвратить

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

Опубликовано 2026-09-05 Обновлено 2026-10-06 Последняя проверка 2026-10-06 13 мин чтения

Восстановление полученных байтов в объекты становится опасным не из-за того, что содержимое испорчено. В некоторых форматах внешние данные сами решают, объект какого типа будет создан.

Почему проверка ввода приходит слишком поздно

Чистый формат данных (JSON / XML)

внешние данные решают только значения → вы получаете их и переносите в свои типы → проверка работает

Формат, восстанавливающий объекты

внешние данные решают значения и тип → во время восстановления выполняется код выбранного типа → выполнение до ваших проверок не доходит

Проверка значений происходит, когда объект уже создан. В формате, который выбирает типы, к этому моменту всё уже кончено.

Большинство проверок ввода спрашивают, допустимо ли значение. Но в формате, восстанавливающем объекты, само восстановление — какой класс создаётся и какой код выполняется по пути — управляется внешними данными. Что-то может успеть выполниться ещё до вызова вашей проверки.

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

Что идёт не так (три пункта OWASP)

Последствия, наблюдавшиеся при атаках на десериализаторы
Отказ в обслуживании
Сделать само восстановление дорогим или сломать его, чтобы остановить приложение
Обход контроля доступа
Объекты, представляющие права или роли, создаются так, как удобно злоумышленнику
Удалённое выполнение кода
Цепочка процедур, вызываемых при восстановлении, приводит к выполнению произвольного кода — самый серьёзный исход

«Наш фреймворк так не делает» — небезопасное допущение

Это история не только о старых языках. Когда мы посчитали уязвимости Next.js в данных NVD, десериализация (CWE-502) оказалась высоко в распределении по CWE, потому что современные фреймворки включают пути, десериализующие тело запроса, в том числе Server Actions (много ли уязвимостей в Next.js и React?). «Я этого не писал» не значит «этого там нет».

Две защиты

1. Не давать ничему выбирать типы (первый выбор)

Формулировка OWASP: «By switching to a pure data format like JSON or XML, you lessen the chance of custom deserialization logic being repurposed towards malicious ends» (переходя на чистый формат данных вроде JSON или XML, вы снижаете вероятность того, что собственная логика десериализации будет использована во вред).

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

2. Подписывать и отвергать неподписанное

Если без собственного формата не обойтись, руководство отмечает, что можно «sign them as part of the serialization process» (подписывать их в процессе сериализации) и «choose not to deserialize any message which didn't have an authenticated signature» (не десериализовать сообщения без проверенной подписи).

Суть в том, чтобы проверять до восстановления. Если сначала восстановить, а потом проверить, может быть уже поздно по указанной выше причине. Весь контроль — в порядке действий.

Некоторые механизмы нельзя защитить настройкой

О BinaryFormatter в .NET OWASP прямо пишет: «the BinaryFormatter type is dangerous and cannot be secured» (тип BinaryFormatter опасен и не может быть защищён).

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

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

Что OWASP называет по языкам

Названо опасным (кратко, с точки зрения защиты)
PHP
Избегайте unserialize(); предпочитайте JSON
Python
pickle / c_pickle, load из PyYAML и jsonpickle названы опасными
Java
Переопределите ObjectInputStream#resolveClass(), чтобы ограничить классы, которые можно восстановить
.NET
BinaryFormatter опасен и не может быть защищён — не используйте его

Общее: удобные встроенные механизмы сохранения объектов в языке — именно то, что никогда не должно получать внешние данные. Они созданы для передачи целых объектов между доверяющими друг другу сторонами и не рассчитаны на враждебного отправителя.

Безопасные и небезопасные API по языкам

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

ЯзыкНе использовать для внешних данныхИспользовать вместоЧто говорит официальная документация
Pythonpickle.load / pickle.loads, marshal, yaml.load (с Loader=yaml.Loader) и yaml.unsafe_load, jsonpickle.decodejson.loads, yaml.safe_loadpickle «is not secure» (небезопасен); если нужна защита от подмены, подписывайте с hmac
PHPunserialize() (даже с заданным allowed_classes)json_decode() / json_encode()Не передавать недоверенный ввод независимо от allowed_classes
RubyMarshal.loadJSON или другой формат, загружающий только базовые типыЗагрузка из недоверенного источника может привести к удалённому выполнению кода
JavaObjectInputStream#readObject, XMLDecoder, XStream#fromXML для внешних данныхПереносить JSON в свои классы (DTO)Если не обойтись, ограничьте разрешённые классы через ObjectInputFilter
.NETBinaryFormatter, SoapFormatter, NetDataContractSerializer, LosFormatter, ObjectStateFormatter, TypeNameHandling в Json.NET со значением, отличным от NoneSystem.Text.Json, XmlSerializer, DataContractSerializerНачиная с .NET 9, BinaryFormatter выбрасывает исключение при использовании

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

# Избегать: сразу превращать полученные байты обратно в объект
order = pickle.loads(request_body)
 
# Использовать: прочитать значения и перенести в свой тип только нужные поля
data = json.loads(request_body)
order = Order(id=int(data["id"]), quantity=int(data["quantity"]))

В безопасном варианте какой класс создать, решает ваш код (Order). Входящие данные решают только значения id и quantity, и к ним применима обычная проверка ввода.

Что проверить в своём коде

1

Перечислите все места, куда сначала попадают внешние данные

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

2

Найдите, какие из них используют формат, восстанавливающий типы

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

3

Перейдите на чистый формат данных

Принимайте JSON или XML и переносите его в свои типы своим кодом. Если при этом проверять схему, заодно закроются неожиданные ключи (то же средство, что и против загрязнения прототипа).

4

Где перейти нельзя, защитите подписью

Если формат нельзя изменить, подписывайте при сериализации и проверяйте подпись перед восстановлением. Сделайте правилом по умолчанию «нет подписи — нет восстановления». С ключом обращайтесь так, как описано в статье чем опасны .env и API-ключи, — никогда не храните его рядом с кодом.

5

Посчитайте восстановление объектов в зависимостях

Даже если вы сами ничего такого не писали, объекты может восстанавливать зависимость. CVE с классификацией CWE-502 появляются постоянно, поэтому поставьте их под автоматический мониторинг (как начать работать с osv-scanner / практическое руководство по устранению CVE).

Как проверить свою среду

Здесь шаг «найти» из списка выше превращён в конкретные команды и настройки. Все проверки только читают ваш собственный репозиторий и ваши собственные серверы.

1

Текстовый поиск по исходному коду

Начните с того, чтобы перечислить всех кандидатов. С ripgrep (rg) поиск выглядит так (grep -rnE принимает те же шаблоны).

# Python
rg -n "pickle\.loads?\(|marshal\.loads?\(|yaml\.load\(|yaml\.unsafe_load|jsonpickle\.decode"
# PHP
rg -n "unserialize\("
# Ruby
rg -n "Marshal\.load"
# Java
rg -n "ObjectInputStream|readObject\(|XMLDecoder|fromXML\("
# .NET
rg -n "BinaryFormatter|SoapFormatter|NetDataContractSerializer|LosFormatter|ObjectStateFormatter|TypeNameHandling"

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

2

Ищите подавленные предупреждения

В .NET использование BinaryFormatter вызывает предупреждение или ошибку SYSLIB0011. Настройка, которая его подавляет, указывает на место, где опасный вызов всё ещё остаётся в коде.

rg -n "SYSLIB0011|CA23[0-3][0-9]" --glob "*.cs" --glob "*.csproj" --glob ".editorconfig" --glob "*.props"

Если нашли #pragma warning disable SYSLIB0011 или этот ID внутри <NoWarn>, выясните, почему он там и когда будет миграция.

3

Включите правила статического анализа

Для Python нужные проверки Bandit — B301 (pickle и подобные), B302 (marshal) и B506 (yaml.load); команда bandit -r . -t B301,B302,B506 запускает только эти три. Для .NET нужные правила анализа кода — серия CA2300 (например, CA2326, которое отмечает TypeNameHandling в Json.NET). CA2326 не включено по умолчанию даже в .NET 10, поэтому включите его в .editorconfig строкой вроде dotnet_diagnostic.CA2326.severity = warning. Запускайте это в CI, и новые вызовы тоже будут остановлены.

4

Для Java проверьте настройки фильтра во время выполнения

Java позволяет во время выполнения ограничить классы, которые может создавать десериализация (фильтры сериализации). Механизм появился в JDK 9 (JEP 290) и стал переключаемым для отдельных контекстов в Java 17 (JEP 415). Для работающего процесса ищите jdk.serialFilter в выводе jcmd <PID> VM.system_properties. Его также можно задать как свойство безопасности в файле java.security, поэтому проверьте оба места. Если его нет ни там, ни там и ваш код не вызывает ObjectInputFilter.Config.setSerialFilter, фильтра на уровне процесса нет.

5

Проверьте зависимости на известные уязвимости

Даже если в вашем коде таких вызовов нет, объекты может восстанавливать зависимость. Получите список известных уязвимостей в зависимостях с помощью osv-scanner или похожего инструмента и в первую очередь обновите те, что классифицированы как CWE-502 (десериализация недоверенных данных).

Частые ошибки и как их исправить

ОшибкаПочему это проблемаИсправление
Доверять данным, потому что «они внутренние» или «я сам сохранил этот файл»Microsoft приводит примеры, когда данные пересекают границу доверия незаметно для разработчика: общие файлы сохранений, облачная синхронизация, скомпрометированный компьютер сотрудникаЕсли до данных может дойти любой путь извне, считайте их внешними. Не восстанавливайте ничего без подписи
Добавлять опасные классы в чёрный список по одномуЭто не помогает против сочетаний, которых нет в списке или которые ещё не известны. Руководство PHP требует не передавать недоверенный ввод даже с allowed_classesСмените формат на JSON или подобный. Только если не обойтись, перечислите разрешённые классы
Сначала восстанавливать, потом проверять содержимоеКод выполняется во время восстановления, поэтому проверка приходит слишком поздноПоставьте проверку подписи перед восстановлением
Подавить предупреждение и считать проблему решённойЕсли заглушить SYSLIB0011 или CA2326, опасный вызов останется на местеИщите подавления и установите срок миграции
Считать, что «это JSON, значит, безопасно»Настройки, позволяющие JSON указывать свои типы, например TypeNameHandling в Json.NET, превращают его в формат, выбирающий типыОставьте TypeNameHandling в значении None (по умолчанию)
Использовать yaml.load в PyYAMLВ документации PyYAML сказано, что yaml.load так же мощен, как pickle, и может вызвать любую функцию PythonЗамените на yaml.safe_load

Реальный пример: восстановление объектов внутри расширения

Бюллетень этого сайта о CVE-2026-45247 описывает внедрение PHP-объектов (CWE-502) в расширении Magento 2 «Mirasvit Full Page Cache Warmer» версий до 1.11.12. Уязвимость позволяет удалённое выполнение кода без аутентификации, имеет оценку CVSS 9.3 и внесена в каталог известных эксплуатируемых уязвимостей CISA (KEV).

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

Позиция сайта: спрашивайте, что разрешено решать вводу, а не правильно ли значение

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

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

Источники (первичные)

  • OWASP Cheat Sheet Series, «Deserialization Cheat Sheet» — cheatsheetseries.owasp.org (три последствия, переход на чистый формат данных, подпись для целостности, замечания по языкам и утверждение о BinaryFormatter — всё из этого документа; проверено 5 сентября 2026 года)
  • Документация Python, «pickle» — docs.python.org (предупреждение «not secure»; подпись с hmac и предпочтение JSON)
  • PyYAML Documentation — pyyaml.org (разница между yaml.load и yaml.safe_load)
  • Руководство PHP, «unserialize» — php.net (не передавать недоверенный ввод независимо от allowed_classes; использовать JSON)
  • Документация Ruby, «Marshal» — docs.ruby-lang.org
  • Microsoft Learn, «Deserialization risks in use of BinaryFormatter and related types» — learn.microsoft.com (столь же опасные типы, рекомендуемые альтернативы, поведение начиная с .NET 9, примеры пересечения границы доверия)
  • Microsoft Learn, «SYSLIB0011» и «CA2326» — SYSLIB0011 / CA2326
  • OpenJDK, «JEP 290: Filter Incoming Serialization Data» и «JEP 415: Context-Specific Deserialization Filters» — JEP 290 / JEP 415
  • Документация Bandit (B301, B302, B506) — bandit.readthedocs.io
  • Таблица по языкам, шаги поиска и частые ошибки сверены с документами выше 6 октября 2026 года

Что почитать дальше

FAQ

QПочему десериализация так опасна? Разве это не просто преобразование данных?
A

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

QКакая защита самая надёжная?
A

Ограничить данные из внешних источников форматом, который не может выбирать типы. OWASP формулирует это так: переходя на чистый формат данных вроде JSON или XML, вы снижаете вероятность того, что собственная логика десериализации будет использована во вред. Если без собственного формата не обойтись, то же руководство отмечает, что можно подписывать сообщения в процессе сериализации и не десериализовать ни одно сообщение без проверенной подписи. Сначала смените формат; если не можете — закрепите его подписью.

QМожно ли сделать это безопасным, ужесточив настройки?
A

Зависит от механизма. OWASP прямо пишет, что тип BinaryFormatter в .NET опасен и не может быть защищён. Некоторые механизмы не становятся безопасными ни от настроек, ни от дополнительных проверок — единственное решение перестать их использовать. Отделяйте то, что можно защитить настройкой, от того, что нельзя. Инструменты, для которых «если правильно использовать, всё нормально» не выполняется, действительно существуют.

QНа что обратить внимание в моём языке?
A

Из того, что OWASP называет прямо: в PHP избегайте unserialize() и предпочитайте JSON; в Python опасны pickle / c_pickle, load из PyYAML и jsonpickle; в Java переопределите ObjectInputStream#resolveClass(), чтобы ограничить классы, которые можно восстановить; в .NET не используйте BinaryFormatter. Общее одно: удобные встроенные механизмы сохранения объектов в языке — именно то, что нельзя направлять на внешние данные.

QКак найти небезопасную десериализацию в своём коде?
A

В три прохода. Сначала текстовый поиск с ripgrep или grep вызовов вроде pickle.loads, unserialize, Marshal.load, ObjectInputStream и BinaryFormatter. Затем включите правила статического анализа (B301, B302 и B506 в Bandit для Python; правила анализа кода серии CA2300 для .NET). Наконец, проверьте зависимости на известные уязвимости инструментом вроде osv-scanner и в первую очередь обновите те, что относятся к CWE-502. Для каждого найденного вызова запишите, откуда приходят получаемые им данные, и сначала исправьте доступные извне.

QС JSON всегда безопасно?
A

Нет, если включить настройку, позволяющую данным самим указывать свои типы. Обычный пример — TypeNameHandling в Json.NET для .NET; правило анализа кода Microsoft CA2326 требует не использовать никаких значений, кроме None. Преимущество JSON в том, что он передаёт только значения, поэтому какой класс создать, решает принимающий код. Проверьте, не включили ли вы настройку, которая убирает это преимущество.