Утро четверга. Вы открываете таск-трекер, а там тикет от сканера зависимостей: «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.