Десериализация: опасный разбор данных
Превращать чужие байты обратно в объекты — операция с подвохом.
Десериализация — восстановление объекта из его сериализованного представления (строки/байтов). Небезопасные форматы при этом могут исполнять код.
В чём опасность
Некоторые форматы сериализации сохраняют не только данные, но и информацию о типах и могут при восстановлении вызывать методы, создавать произвольные объекты, запускать конструкторы. Если такие данные пришли от пользователя, атакующий формирует payload, который при десериализации выполняет код на сервере — это одна из самых тяжёлых уязвимостей (insecure deserialization).
Чтобы прочувствовать масштаб, сравните «бедный» и «богатый» форматы. Когда вы разбираете JSON, на выходе всегда получаются простые значения: числа, строки, булевы, списки и словари — пассивные данные, которые ничего не делают сами по себе. Когда вы разбираете данные «богатым» десериализатором, способным воссоздавать объекты любого типа, восстановление перестаёт быть пассивным: процесс материализует объекты и попутно исполняет их код. Разница не в удобстве, а в том, что во втором случае входные байты фактически управляют тем, какой код запустится в вашем процессе.
Ключевая мысль: уязвима не «плохая библиотека», а сама модель доверия. «Богатая» сериализация прекрасно подходит для общения компонента с самим собой — например, передать объект между процессами одного приложения, где обе стороны вам доверены. Проблема возникает ровно тогда, когда такие данные пересекают границу доверия и приходят снаружи. Поэтому решение формулируется не как «запретить формат», а как «не применять формат, исполняющий код, к недоверенному вводу».
// Уязвимо: разбор недоверенных данных «богатым» десериализатором,
// способным инстанцировать произвольные типы и вызывать их код
obj = nativeDeserialize(request.body); // payload может выполнить команду
// (характерно для pickle, нативной сериализации объектов, небезопасного YAML и т.п.)
Защита 1: безопасные форматы данных
Для обмена данными используйте форматы, которые описывают только данные, без типов и поведения: JSON — основной выбор. Он не несёт исполняемой нагрузки: из него получаются простые значения, списки и словари.
| Формат | Безопасен для недоверенного ввода? |
| JSON | да (только данные) |
| YAML «safe load» | да, в безопасном режиме |
| YAML обычный load | нет — может создавать объекты |
| нативная сериализация объектов (pickle и аналоги) | нет — исполняет код |
// Безопасно: JSON несёт только данные
data = jsonParse(request.body); // числа, строки, списки, словари
// Если нужен YAML — только безопасный режим
data = yamlSafeLoad(request.body); // запрещает произвольные типы
Защита 2: валидируйте результат по схеме
Даже безопасный формат даёт структуру, которую вы не контролировали. После разбора прогоните результат через схему: ожидаемые поля, типы, диапазоны. Это тот же allowlist, перенесённый на форму данных, — лишние поля отбрасываются, неверные типы отвергаются.
Стоит чётко разделить два уровня защиты, потому что их часто путают. Безопасный формат гарантирует, что разбор данных не исполнит код — это про класс «insecure deserialization». Но он ничего не говорит о том, осмысленны ли сами данные: JSON с полем "role": "admin" или с отрицательной ценой совершенно валиден как JSON. Валидация по схеме — это уже второй уровень, про прикладную корректность и доверие к содержимому. Один уровень не заменяет другой: безопасный формат закрывает исполнение кода, схема закрывает «мусорные» и враждебные данные, и нужны оба.
Особенно осторожно стоит относиться к удобному, но рискованному приёму «слепого присваивания»: взять разобранный объект и целиком наложить его на вашу модель данных, доверившись именам полей из ввода. Так в модель просачиваются поля, которые пользователь менять не должен (это разновидность проблемы массового присваивания). Схема с явным перечнем разрешённых полей решает и это: вы заранее описываете, что вообще принимаете, а всё прочее во входных данных просто игнорируется.
// После безопасного парсинга — валидация структуры
data = jsonParse(body);
order = schema.parse(data); // отвергнет лишние/неверные поля, приведёт типы
Как работает под капотом: gadget chains
Атаки на небезопасную десериализацию редко «исполняют код напрямую». Обычно payload выстраивает цепочку гаджетов: использует уже присутствующие в приложении и его библиотеках классы так, что их штатные методы, вызванные при восстановлении объекта, в нужной последовательности приводят к опасному действию. Поэтому защита — не «фильтровать плохое», а не давать недоверенным данным инстанцировать произвольные типы: для этого и нужны форматы только-для-данных.
Из устройства «цепочки гаджетов» следует важный практический вывод о денилистах. Соблазнительно «закрыть» опасную десериализацию списком запрещённых классов — но это снова проигрышная стратегия. Гаджеты находят в самых обычных, ничем не подозрительных библиотеках, и их состав меняется при каждом обновлении зависимостей: завтра очередной апдейт принесёт новую пригодную цепочку, которой не было в вашем чёрном списке. Надёжно работает только противоположный, позитивный подход: ограничить множество допустимых типов до явного минимума (allowlist) или, что обычно проще и надёжнее, вообще перейти на формат, который не умеет создавать объекты.
Полезно помнить, что десериализация — это не только очевидный «нативный» формат. Под ту же категорию риска попадают «небезопасный» режим загрузки YAML, некоторые механизмы привязки XML к объектам и любые «умные» парсеры, которые по входным данным решают, объект какого типа создать. Признак опасности один и тот же: восстановление способно само выбирать и создавать типы. Если он есть — данных извне такому механизму скармливать нельзя.
Частые ошибки
- Нативная сериализация для внешних данных. Удобно внутри, опасно на границе доверия.
- Обычный YAML-load на пользовательском вводе. Используйте safe-режим.
- Доверять структуре после парсинга. Валидируйте по схеме.
- Денилист «плохих классов». Обходится новыми гаджетами; ограничивайте типы allowlist'ом или форматом.
- Считать безопасный формат достаточным. JSON не исполнит код, но и не проверит смысл данных — нужна валидация по схеме.
- Слепое присваивание разобранных данных на модель. Так просачиваются поля, которые пользователь менять не должен; принимайте только явно разрешённые поля.
Итоги
- Небезопасные форматы при десериализации могут исполнять код — это критично для внешнего ввода.
- Для обмена данными используйте только-для-данных форматы (JSON, safe-YAML).
- После разбора валидируйте структуру по схеме.
- Не позволяйте недоверенным данным создавать произвольные типы.