Разбор CVE-2026-83557 в jackson-databind: почему не всех CVE надо бояться

Утро четверга. Вы открываете таск-трекер, а там тикет от сканера зависимостей: «jackson-databind, CVE-2026-83557, критично, патчить». Ссылка на GitHub-бюллетень, CVSS, слово «полиморфная десериализация», и через двадцать минут у вас уже согласован hotfix на прод в пятницу в 18:00.
Стоп. Я проделал обратное: взял описание, собрал работающий эксплойт и выполнил его на уязвимой и пропатченной версиях. Рассказываю, почему для большинства проектов тут хватает тикета «в ближайший релиз», без всякого горения. И заодно о том, почему не каждой CVE из сканера стоит верить на слово.
Что вообще случилось
Jackson умеет в полиморфную десериализацию: в JSON кладется идентификатор типа, и библиотека по нему инстанцирует нужный класс. Выглядит это так:
public class Holder { @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY) public Comparable<?> value;
}
{"value": ["java.io.File", "/etc/passwd"]}
Это формат WRAPPER_ARRAY: первый элемент массива служит именем класса, второй содержит данные, из которых Jackson соберет экземпляр.
Понятно, что нельзя принимать произвольные имена классов из непроверенного JSON, поэтому в Jackson есть механизм PolymorphicTypeValidator, который решает, какой подтип допустим для данного базового типа. Валидатор по умолчанию называется DefaultBaseTypeLimitingValidator. Задумка у него такая: держать черный список «небезопасных» базовых типов, с полиморфизмом на которых Jackson решил бороться. Позиций в списке девять, от банальных Object и Serializable до javax.sql.DataSource. Все, что в список не попало, одобряется - проверять подтип валидатор не умеет вообще.
Вот и вся CVE: в списке забыли java.lang.Comparable, а зря. Comparable реализует куча JDK-классов, от численных оберток до String и File. Если есть полиморфное свойство с базовым типом Comparable, атакующий подсунет type id почти любого из них, и Jackson молча создаст экземпляр.
Версия jackson-databind у вас в проекте с огромной вероятностью попадет под диапазон уязвимых (это 2.11-2.18.9, 2.19.0-2.21.5 и 2.22.0-2.22.1). Поэтому не будем тратить время на версию, а сразу посмотрим на два реальных барьера в вашем коде.
Барьер первый: включенный флаг защиты
DefaultBaseTypeLimitingValidator подключается только при включенном MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES. По умолчанию этот флаг выключен.
Что происходит без флага? Для обычного свойства с @JsonTypeInfo(Id.CLASS) Jackson использует LaissezFaireSubTypeValidator, который разрешает все. Но это не CVE-2026-83557. Это давно известное и документированное поведение Id.CLASS.
Получается парадокс под ударом оказались только те, кто явным образом включал защитный флаг. Включить его и не перепроверить руками список базовых типов - вот и весь сценарий.
Барьер второй: у вас должно быть Comparable-свойство
Валидатор смотрит на объявленный тип свойства, и вот тут черный список работает: Object и Serializable он честно блокирует, payload с ними отваливается еще при построении десериализатора. То есть CVE касается только свойств, объявленных как Comparable<?> или сырой Comparable, с аннотацией @JsonTypeInfo.
Посмотрите на свои DTO: конкретные типы, коллекции, бизнес-модели. Полиморфное поле типа Comparable, которому подсовывают произвольный type id, встретишь разве что в проекте из серии «мы сериализуем сортируемые значения любых типов и радостно разбираем их обратно». Это крайне редкий паттерн, которого в большинстве сервисов просто нет.
Допустим, все сошлось. Что получит атакующий?
Показать проще, чем объяснять. Вот весь «уязвимый» код приложения, больше для атаки ничего не нужно:
public class Holder { @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY) public Comparable<?> value;
}
// где-то в обработчике запроса
Holder h = mapper.readValue(requestBody, Holder.class);
Атакующий присылает в requestBody одну строчку:
{"value": ["java.io.File", "/etc/passwd"]}
И после парсинга в поле h.value лежит живой java.io.File. Jackson, по сути, выполнил за атакующего вот это:
Comparable<?> value = new File("/etc/passwd");
Библиотека взяла имя класса из JSON, нашла его в classpath приложения, сконструировала и записала в поле. Единственные ограничения: класс должен реализовывать Comparable и присутствовать в classpath. Этот примитив называется контролируемым инстанцированием: атакующий выбирает класс и данные, но исполняемого кода в конструкторе сам написать не может.
У меня на 2.19.4 эксплойт выдает:
[1] control: Serializable-typed property + payload {"value":["java.io.File","/etc/hosts"]} -> rejected as expected (Serializable IS in the denylist)
[2] exploit: Comparable-typed property + same payload -> VULNERABLE: accepted java.io.File under Comparable base type: instantiated java.io.File, path = /etc/hosts, exists = true
Сравните прогоны [1] и [2]: payload один и тот же, различие только в объявленном типе свойства, а результат противоположный. Вся CVE видна в этих двух строчках вывода.
Файл создан, exists даже true. И… все. Автор бюллетеня честно пишет, что чистого эксплойта для выполнения кода не нашел. Я тоже не нашел и вот почему особенно не искал. Почему в denylist попали именно базовые типы вроде Runnable, logging.Handler, Referenceable или DataSource? Они исторически обвешаны опасным кодом, JNDI-лукапами и кредами. У Comparable конструкторы скучные: сравнение двух объектов само по себе никого не взломает.
Это не значит, что дыра безобидна в принципе: инстанцирование непроверенных классов может стать ступенью для будущей цепочки атаки, а оракул существования файлов сам по себе приятного мало. CVSS 5.6 (Moderate) оценивает это адекватно. Адекватность теряется не в бюллетене, а в корпоративной цепочке «сканер, тикет, срочно».
Бонус: патч не бесплатный
Я прогнал тот же PoC на 2.21.6. Comparable в denylist добавили; фикс вышел в 2.18.10, 2.21.6 и 2.22.2, выбирайте свою ветку. Но вот нюанс: базовый тип теперь запрещен целиком, вместе с легитимными подтипами. Мой кейс с собственным классом SafeThing implements Comparable<SafeThing> на пропатченной версии падает с InvalidDefinitionException. Если у вас есть легитимная полиморфия на Comparable, после обновления она отвалится, и ее придется выносить на явный BasicPolymorphicTypeValidator с белым списком.
Важная оговорка: ломается это только при включенном BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES. С выключенным флагом патч ничего не меняет, десериализатор ведет себя одинаково и до, и после обновления. Под раздачу снова попадают только те, кто защищался.
То есть принцип «пришла CVE, катим патч на все подряд» и здесь дает сбой: обновление может уронить функциональность там, где сама CVE никогда бы не сработала.
Пять минут на проверку
# 1. Включен ли защитный флаг (без него эта CVE к вам неприменима в принципе)
grep -rn "BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES" src/
# 2. Есть ли полиморфные Comparable-поля: файлы, где встречаются и @JsonTypeInfo, и Comparable<
grep -rln "@JsonTypeInfo" src/ | xargs grep -l "Comparable<"
Насчет второй команды: слово Comparable в живом проекте встречается часто (от компараторов до сортировок), поэтому скрипт выдает кандидатов на ручной просмотр, а не окончательный вердикт. Смотреть надо места, где @JsonTypeInfo стоит на свойстве, объявленном как Comparable.
Выключенный флаг или отсутствие Comparable-полей: любой из двух ответов закрывает тикет со спокойной совестью. Оба проверяются быстрее, чем пишется комментарий «фиксим, критично!!!».
Почему это важнее, чем одна CVE
Сканер зависимостей видит две вещи: версию библиотеки и номер CVE. Он не видит конфигурацию маппера и структуру ваших DTO. А именно эти вещи решают, существует ли уязвимость в реале. В случае CVE-2026-83557 на пути уязвимости стоят два конкретных барьера в коде и конфигурации.
Для контраста возьмем Log4Shell. Там условием был пользовательский ввод в лог. Логировать то, что присылает пользователь, делает почти каждое приложение, отдельно стараться не надо. Поэтому Log4Shell стреляла массово, а эта CVE - нет.
Jackson патчить надо, разумеется. Без драм, с тестами, в ближайший релиз. А если ваша корпоративная машина разводит пожар из каждой Moderate-CVE, лечить надо машину.
Все эксперименты из статьи воспроизводимы: PoC на сотню строк Java, версии 2.19.4 (уязвимая) и 2.21.6 (пропатченная), JDK 25.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.