В июне 2026 года был опубликован список из 71 уязвимости в экосистеме Spring. Из них 31 получила уровень опасности High, 36 — Medium и четыре — Low. Для сравнения: за весь 2025 год было обнаружено 16 уязвимостей.

Из общего списка уязвимостей мы выбрали три, в которых сочетаются относительно понятный путь до уязвимого кода и серьёзные последствия. Нас интересовало, как ошибка проявляется в распространённых сценариях: при обработке JSON Patch, аутентификации через LDAP и передаче сообщений через Kafka.

Разберём чуть подробнее три самые интересные уязвимости по соотношению «достижимость × последствия»:

  1. CVE-2026-41729 — SpEL-инъекция через ключ Map в JSON Patch (Spring Data REST).

  2. CVE-2026-41720 — обход аутентификации пустым паролем в Spring LDAP (unauthenticated bind).

  3. CVE-2026-41731 — небезопасная десериализация заголовков в Spring for Apache Kafka (префиксное сопоставление доверенных пакетов).

1. CVE-2026-41729: SpEL-инъекция через ключ Map в JSON Patch

CVSS 3.1: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

В чём суть

Spring Data REST принимает JSON Patch (RFC 6902) с Content-Type: application/json-patch+json. Каждая операция патча (add, replace, test и т.д.) задаёт путь к полю в формате JSON Pointer. Когда сохраняемый объект (корневой агрегат) содержит свойство-Map, сегмент пути JSON Pointer трактуется как ключ Map и без экранирования подставляется в SpEL-выражение для обращения к значению по этому ключу. Специально сформированный ключ нарушает структуру выражения metadata['<ключ>']: закрывает строковый литерал раньше времени и позволяет добавить другое SpEL-выражение, которое будет вычислено в контексте корневого агрегата.

Затронуты оба пути обработки: чтение (когда патч обращается к текущему значению) и запись (когда патч изменяет данные).

Класс ошибки — CWE-917 (недостаточная нейтрализация специальных элементов при построении выражения).

Технический механизм

Для обычного объекта путь /profile/name преобразуется в последовательность обращений к Java-свойствам (profile.name). Если свойство имеет тип Map, к его значению нужно обратиться по ключу:

Путь JSON Pointer:   /metadata/<map-key>

Выражение SpEL:      metadata['<map-key>']

Проблема в том, что <map-key> не экранируется перед тем, как попасть внутрь строкового литерала '...':

Ключ (ожидание):        "config"

Результат:              metadata['config']

Ключ (атака):           "x'] + T(java.lang.System).getProperty('os.name') + ['"

Результат (уязвимо):    metadata['x'] + T(...).getProperty('os.name') + ['']

                        └─ вне намеренного ключа вычисляется произвольный SpEL-код

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

Поток данных при атаке:

HTTP PATCH

  → Content-Type: application/json-patch+json

  → разбор операций JSON Patch

  → извлечение пути (JSON Pointer)

  → определение, что свойство имеет тип Map

  → построение конструкции metadata['<ключ>'] без экранирования ключа Map

  → вычисление SpEL в контексте корневого агрегата

  → чтение или изменение данных за пределами намеренного ключа

Ущерб для организации

  • Доступ к данным за пределами указанного ключа — выражение вычисляется в контексте всего корневого агрегата, поэтому доступны все его поля и вложенные объекты, даже не предназначенные для выдачи (C:H).

  • Несанкционированное изменение данных — путь записи позволяет менять не те поля, которые указаны в патче, в том числе в обход ограничений «только для чтения» модели (I:H).

  • Потенциальное выполнение кода — при наличии в контексте вычисления полезных классов выражение может дойти до вызова произвольных методов JVM (вектор RCE-класса).

  • Высокая достижимость — триггер не требует разработки специфичного кода: достаточно обычной модели с Map-полем и стандартного Spring Data REST, поэтому уязвимых приложений много, а барьер атаки AC:L/PR:L низкий.

  • Для организации: утечка персональных данных (инцидент по 152-ФЗ, уведомление регулятора, штрафы), тихая порча доменных данных (позднее обнаружение через аудит, а не через атаку), мошенничество при компрометации платёжных/залоговых полей.

Итог: самая опасная из трёх по последствиям — потеря конфиденциальности и целостности на высоком уровне с низким порогом эксплуатации, при том что JSON Patch открыт по умолчанию. Усугубляется тем, что первый шаг эксплуатации не является явной аномалией для центра мониторинга информационной безопасности.

2. CVE-2026-41720: обход аутентификации пустым паролем в Spring LDAP

CVSS 3.1: AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

В чём суть

Реализации DirContextAuthenticationStrategy в Spring LDAP не отклоняют bind-запрос, в котором указано непустое имя пользователя в паре с пустым (null) паролем. По RFC 4513 §5.1.2 такой запрос называется unauthenticated bind: клиент сообщает имя, но не предъявляет секрет. Если LDAP-сервер разрешает такой bind, проверка пароля фактически не выполняется.

Ранний аналог в Spring Security — CVE-2014-0079 (та же первопричина: empty-password bind прошёл проверку).

Технический механизм

Протокол LDAP различает три случая:

Bind-запрос

Пароль

Семантика

Пустой DN

пустой

Анонимный доступ (anonymous)

Непустой DN

пустой

Unauthenticated bind

Непустой DN

непустой

Обычная проверка пароля

Сервер с ослабленной конфигурацией (например, разрешённый allow bind anon dn в OpenLDAP / 389 DS) может признать unauthenticated bind успешным, не проверив пароль. Spring LDAP на клиенте воспринимает успешный bind как успешную аутентификацию и не сверяет «пароль был» — стратегия просто не фильтровала пустой пароль до отправки запроса.

Уязвимый путь затрагивает все способы аутентификации через:

  • AbstractContextSource (и его наследники DefaultSpringSecurityContextSource);

  • LdapTemplate.authenticate(...);

  • LdapClient.authenticate(...).

Поток атаки:

Атакующий знает имя пользователя (admin / сервисный аккаунт / пользователь)

  → отправляет bind:  dn=uid=<username>,ou=...,dc=corp  password=""

  → Spring LDAP не отбрасывает запрос как «пустой пароль»

  → сервер выполняет unauthenticated bind и возвращает success

  → приложение считает пользователя аутентифицированным

Ущерб для организации

  • Компрометация корпоративного поставщика идентификации — LDAP часто обслуживает всю экосистему: одну атаку можно переиспользовать против любого подключённого приложения.

  • Доступ под привилегированными учётками — сервисные и админ-аккаунты становятся самой привлекательной целью, т.к. не привязаны к человеческим процессам MFA/смены пароля.

  • Отсутствие следов — аутентификация формально «успешна», события входа не содержат признаков аномалии; SIEM и блокировки по числу неудачных попыток не срабатывают (попытка не «неудачная»).

  • Латеральное движение и долгое присутствие — сессия от имени легитимного пользователя позволяет спокойно обходить продуктовую защиту приложений.

Итог: критичность в слое идентификации, как только сервер разрешает unauthenticated bind.

3. CVE-2026-41731: небезопасная десериализация заголовков в Spring for Apache Kafka

CVSS 3.1: AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

В чём суть

JsonKafkaHeaderMapper и устаревший DefaultKafkaHeaderMapper сопоставляли заголовки типа сообщения со списком «доверенных пакетов» по префиксу: доверие одному пакету автоматически означало доверие всем его подпакетам. В сочетании со стандартной Jackson-десериализацией бинов продюсер мог подложить в заголовок имя произвольного JDK-класса — консьюмер десериализовал его, включая классы, чьи конструкторы несут побочные эффекты (выделение файловых дескрипторов, создание пулов потоков).

Класс ошибки — CWE-502 (десериализация недоверенных данных) с первопричиной «некорректная проверка доверенного диапазона по префиксу».

Технический механизм

Spring for Apache Kafka передаёт информацию о типе через заголовки сообщения: TypeId, KeyTypeId, ContentTypeId. Тип-информацию из этих заголовков читают оба механизма JSON-десериализации:

  • JsonDeserializer — низкоуровневый десериализатор консьюмера (spring.kafka.consumer.value-deserializer) — снимает заголовок напрямую;

  • JsonMessageConverter (например, ByteArrayJsonMessageConverter) — преобразователь уровня контейнера прослушивания, за которым уже стоит байтовый десериализатор.

Оба пути проверяют указанный в заголовке класс против списка доверенных пакетов через маппер заголовков (JsonKafkaHeaderMapper или устаревший DefaultKafkaHeaderMapper).

Уязвимость в логике проверки:

Доверенные пакеты (конфигурация):   com.example.app

Проверка по префиксу (уязвимо):     com.example.app.SomeType  → OK

                                    com.example.appx.Evil    → OK (префикс совпал!)

                                    java.lang.ThreadPool     → OK, если доверен java.*

                                    java.io.FileOutputStream → OK, если доверен java.

Доверие по префиксу означает: если в списке есть com.example или (частая практика) java.util, java.io, java.lang либо корневой пакет приложения, то «пропускается» весь диапазон подпакетов — включая стандартные библиотечные классы JVM. Дальше работает десериализация по умолчанию: Jackson инстанцирует указанный класс через конструктор, а любой класс с побочным эффектом в конструкторе выполняет его прямо при чтении сообщения.

Поток атаки:

Продюсер (недоверенный, имеет доступ на запись в топик)

  → публикует сообщение с заголовком TypeId: java.io.FileOutputStream / java.util.concurrent.ScheduledThreadPoolExecutor ...

  → Kafka доставляет сообщение в консьюмер (@KafkaListener)

  → JsonKafkaHeaderMapper проверяет заголовок типом против доверенных пакетов по префиксу

  → проверка проходит («доверен java.*»)

  → Jackson создаёт экземпляр указанного класса

  → побочный эффект конструктора срабатывает в процессе консьюмера

Ущерб для организации

  • RCE-класс при широком доверенном диапазоне — десериализация произвольных типов в процессе JVM (C:H/I:H/A:H): чтение/изменение данных на узле консьюмера и потенциальное исполнение кода.

  • Латентность и «вслепую» — сообщение лежит в топике и срабатывает в момент обработки любым из потребителей; источник отделён во времени от последствия, что сильно затрудняет связывание инцидента с конкретным событием.

  • Каскадный охват — в событийно-ориентированной архитектуре один скомпрометированный продюсер отравляет сообщения всем консьюмерам топика сразу: неделя расследований вместо одного запроса.

  • Тихий отказ в обслуживании через ресурсы — исчерпание файловых дескрипторов или потоков выглядит как падение производительности «на ровном месте», а не как атака.

  • Для организации: в микросервисной Spring-системе Kafka — ядро обмена, поэтому доверенный контур «производители → топик → потребители» должен быть строгим; размытие этой границы из-за широких доверенных пакетов обнуляет изоляцию сервисов.

Итог: самый «серьёзный по классу» вектор из трёх — десериализация недоверенных данных с потенциальным RCE у каждого потребителя, что особенно критично для событийно-ориентированных архитектур, где Kafka является системообразующим компонентом.

Комментарии (0)