Обе половины этой атаки публичны уже десять лет. HPACK-бомба — известный способ раздуть память сжатыми заголовками. Удержание соединения через нулевое окно управления потоком — классический медленный DoS в духе Slowloris. По отдельности с обоими давно научились жить. Но если склеить их в одном HTTP/2-соединении, получается HTTP/2 Bomb: один клиент с домашнего канала на 100 Мбит/с съедает 32 ГБ памяти сервера за 10–45 секунд и делает его недоступным. Уязвимы дефолтные конфигурации nginx, Apache, Microsoft IIS, Envoy и Cloudflare Pingora.
Самое интересное — не урон, а то, что комбинацию, лежавшую на поверхности десятилетие, первым собрал не человек, а ИИ-агент, прочитавший исходники серверов.
В статье
- Два безобидных примитива
- Склейка: как работает атака
- Почему это валит серверы
- Кто уязвим и что пропатчено
- Почему khorost.tech не тронут: Angie
- Как проверить и защитить свой сервер
- Мета: как такое находят
- Итог
- Источники
Два безобидных примитива
Чтобы понять атаку, нужно разобрать две части, каждая из которых сама по себе — легитимная механика протокола.
HPACK: сжатие заголовков и dynamic table
HTTP/2 сжимает заголовки схемой HPACK (RFC 7541). Ядро HPACK — таблицы индексов. Статическая таблица содержит 61 предопределённую пару (:method: GET, :status: 200 и т. д.). А dynamic table наполняет сам клиент: он один раз отправляет полный заголовок с флагом «занеси в таблицу», после чего может ссылаться на него одним байтом — индексом.
Идея — экономия трафика: повторяющиеся заголовки не гонять по сети целиком. Но у неё есть обратная сторона. Индексная ссылка занимает 1 байт на проводе, а на сервере разворачивается в полноразмерное поле, под которое выделяется память. Это встроенный амплификатор: маленький вход → большой выход. RFC 7541 §7.3 честно предупреждает, что декодер может исчерпать память, и рекомендует ограничивать размер таблицы. Но ограничивают обычно размер таблицы, а не число ссылок на её элементы в одном запросе.
Flow control: клиент может сказать «не присылай ничего»
Второй примитив — управление потоком (flow control, RFC 9113 §6.9). У каждого стрима и у соединения есть окно: сколько байт данных получатель готов принять. Отправитель не имеет права выйти за окно. Окном управляет получатель, увеличивая его фреймами WINDOW_UPDATE.
Здесь ловушка симметрична: клиент, выступая получателем ответа, может объявить нулевое окно. Тогда сервер, сформировав ответ, не может его отправить — и вынужден держать данные (и все связанные аллокации) в памяти, ожидая, пока клиент откроет окно. Это ровно та механика, на которой строится Slowloris: не завалить сервер объёмом, а заставить его ждать, удерживая ресурсы.
Ни то ни другое поодиночке не смертельно. Амплификация HPACK ограничена: развернул заголовки — обработал запрос — освободил память. Нулевое окно держит соединение, но само по себе почти ничего не аллоцирует. Смертельной становится комбинация.
Склейка: как работает атака
Атака соединяет амплификацию HPACK с удержанием через flow control, добавляя третий трюк для обхода лимитов.
- Подкачка dynamic table. Клиент открывает HTTP/2-соединение и заносит в dynamic table минимальный заголовок.
- Лавина ссылок. В одном запросе клиент шлёт тысячи однобайтовых индексных ссылок на этот заголовок. Каждая ссылка — 1 байт на проводе, но сервер под каждую разворачивает поле и заводит служебную структуру. Так рождается амплификация.
- Нулевое окно. Клиент заранее выставил нулевое flow-control окно на ответ. Сервер разобрал гигантский набор заголовков, начал формировать ответ — и застрял: отправить не может, освободить аллокации до завершения обмена тоже. Память запинена.
- Сброс таймаутов. Чтобы сервер не разорвал «висящее» соединение по тайм-ауту, клиент периодически шлёт крошечные
WINDOW_UPDATE-фреймы. Формально соединение живо и прогрессирует — таймер простоя сбрасывается, аллокации держатся столько, сколько позволяет конфиг сервера. - Масштабирование. Клиент открывает такие соединения десятками. Память сервера выедается за секунды.
в полное поле + служебную структуру
память растёт лавинообразно Note over A,S: 3. Удержание S-->>A: ответ готов, но окно = 0 → отправить нельзя Note over S: аллокации запинены в памяти loop каждые несколько секунд A->>S: WINDOW_UPDATE (крошечный) Note over S: таймаут простоя сброшен
память НЕ освобождается end Note over A,S: 4. Масштаб: десятки соединений → OOM
sequenceDiagram
participant A as Атакующий
participant S as Сервер (HTTP/2)
Note over A,S: 1. Подготовка
A->>S: HEADERS: занести заголовок в dynamic table
A->>S: SETTINGS: SETTINGS_INITIAL_WINDOW_SIZE = 0 (окно ответа)
Note over A,S: 2. Амплификация
A->>S: HEADERS: тысячи 1-байтовых индексных ссылок
Note over S: разворачивает каждую ссылку
в полное поле + служебную структуру
память растёт лавинообразно
Note over A,S: 3. Удержание
S-->>A: ответ готов, но окно = 0 → отправить нельзя
Note over S: аллокации запинены в памяти
loop каждые несколько секунд
A->>S: WINDOW_UPDATE (крошечный)
Note over S: таймаут простоя сброшен
память НЕ освобождается
end
Note over A,S: 4. Масштаб: десятки соединений → OOM
Обход лимита полей: cookie-crumb splitting
Разумная защита — ограничить число полей заголовков в запросе. Но у атаки есть обход. RFC 9113 §8.2.3 явно разрешает разбивать заголовок Cookie на отдельные поля — по одному «крамбу» (crumb) на поле — ради лучшего сжатия HPACK. Уязвимые серверы не считали эти крамбы против лимита полей: формально это один логический Cookie, а физически — тысячи отдельных индексных ссылок. Через Cookie атакующий проносил лавину мимо счётчика заголовков. Именно поэтому корректный патч — считать все поля, включая cookie-крамбы, а не только «настоящие» заголовки.
Почему это валит серверы
Ключевой инсайт авторов атаки: спецификация оценивает риск памяти как коэффициент амплификации, но ratio — лишь половина уравнения. Вторая половина — как долго удерживается аллокация. Нулевое окно превращает разовый всплеск в постоянную нагрузку: память не откатывается, пока держится соединение.
Отсюда и разброс: серверы с низкой амплификацией всё равно падают, просто медленнее. Реальные замеры авторов (Calif) по дефолтным конфигурациям:
| Сервер | Амплификация | Урон |
|---|---|---|
| Envoy 1.37.2 | ~5700 : 1 | ~32 ГБ за ~10 с |
| Apache httpd 2.4.67 | ~4000 : 1 | ~32 ГБ за ~18 с |
| nginx 1.29.7 | ~70 : 1 | ~32 ГБ за ~45 с |
| Microsoft IIS | ~68 : 1 | ~64 ГБ за ~45 с |
| Cloudflare Pingora | уязвим | — |
Даже «скромные» ~70 : 1 у nginx означают: один байт на проводе → около 70 байт памяти сервера. При тысячах ссылок в запросе и десятках удерживаемых соединений этого хватает, чтобы за минуту выесть память. Память здесь уходит не на декодированный текст заголовков, а на служебный учёт (bookkeeping) под каждую ссылку — внутренние структуры пула. Поэтому и работает лимит по числу полей: он бьёт по количеству учётных записей, а не по их размеру.
Кто уязвим и что пропатчено
Уязвимы дефолтные конфигурации. Статусы ниже — по состоянию на 6 июля 2026 года (атака раскрыта в июне 2026). Вендорские бюллетени быстро обновляются, поэтому перед действиями сверяйтесь с первоисточником:
| Сервер | Статус | Фикс / митигация |
|---|---|---|
| nginx | Пропатчен | 1.29.8 — директива max_headers (дефолт 1000), импортирована из freenginx |
| Apache httpd | Пропатчен | mod_http2 2.0.41 — cookie-заголовки теперь считаются против LimitRequestFields (CVE-2026-49975) |
| Envoy | Пропатчен | 1.35.11 и 1.36.7 (CVE-2026-47774) |
| Microsoft IIS | Без патча | Отключить HTTP/2 или фронтить прокси с лимитом полей |
| Cloudflare Pingora | Без патча | То же |
| Angie | Не подвержен | max_headers с версии 1.8.0 (2024) — подробнее ниже |
Red Hat трекает связку под бюллетенем RHSB-2026-007: CVE-2026-49975 — Apache httpd, CVE-2026-47774 — Envoy; upstream nginx отдельного CVE не присваивал, хотя фикс доступен. Статусы «без патча» для IIS и Pingora верны на указанную дату и со временем изменятся — проверяйте адвайзери вендора. Общий знаменатель всех фиксов один: ограничить число полей заголовков в запросе, считая в том числе cookie-крамбы. Это и есть корневая защита — она рубит амплификацию на входе, ещё до того, как нулевое окно успеет запинить память.
Почему khorost.tech не тронут: Angie
Этот сайт работает на Angie — форке nginx, который раздаёт статику и проксирует API. И вот приятная деталь: Angie не подвержен HTTP/2 Bomb, потому что нужную защиту он получил задолго до атаки.
Директива max_headers, ограничивающая число полей заголовков в запросе, появилась в Angie ещё в версии 1.8.0 от 19 декабря 2024 года. Строка из changelog:
The
max_headersdirective that limits the number of HTTP request header fields to better protect against DoS attacks.
Механика та же, что позже добавили в nginx mainline (1.29.8, 2026) и что оба проекта унаследовали из freenginx Максима Дунина: жёсткий потолок на количество полей заголовков в одном запросе. Как только счётчик полей — включая cookie-крамбы — упирается в лимит, запрос отклоняется, и лавина индексных ссылок просто не создаётся. Амплификация не набирается, пинить в памяти нечего.
Показательна разница в сроках: то, что nginx mainline получил в 2026-м под давлением конкретной атаки, в Angie лежало проактивно с конца 2024-го — за полтора года до раскрытия. Это ровно тот случай, когда выбор веб-сервера с более консервативной политикой лимитов окупается: защита сработала до того, как угроза стала публичной.
Проверить свою версию:
angie -v
# angie version: Angie/1.8.0 ← и новее — max_headers доступнаКак проверить и защитить свой сервер
Ниже — оборонительные проверки и конфиги. Готового эксплойта здесь нет и не будет: смысл материала — закрыть дыру, а не вооружить атакующего.
Проверить, включён ли лимит полей
Быстрый безопасный тест — отправить запрос с заведомо большим (но конечным) числом валидных заголовков и посмотреть, отклонит ли его сервер. Это не бомба: обычные заголовки, обычный запрос, никакого нулевого окна.
# 2000 обычных заголовков поверх HTTP/2. Сервер с лимитом ~1000
# должен ответить 431/400/RST, а не молча всё принять.
python3 - <<'PY'
import httpx # httpx умеет HTTP/2: pip install "httpx[http2]"
headers = {f"x-probe-{i}": "1" for i in range(2000)}
try:
r = httpx.get("https://example.com/", headers=headers, http2=True, timeout=10)
print("status:", r.status_code, "| http:", r.http_version)
except Exception as e:
print("отклонено на уровне протокола:", type(e).__name__)
PYЕсли сервер спокойно принимает тысячи полей — лимит не настроен.
Конфиги: включить лимит полей
http {
# Потолок на число полей заголовков в запросе (дефолт 1000).
# Считает и cookie-крамбы — ровно то, что рубит HTTP/2 Bomb.
max_headers 1000;
}# Обновиться до mod_http2 2.0.41+, где cookie-крамбы считаются
# против LimitRequestFields, и задать разумный лимит полей:
LimitRequestFields 100http2_protocol_options:
max_headers_count: 100 # потолок числа полей заголовковЕсли патча нет (IIS, Pingora)
- Отключить HTTP/2 там, где он не обязателен: атака живёт именно в HPACK + flow control HTTP/2.
- Поставить перед сервером фронт (Angie/nginx/Envoy свежих версий) с лимитом полей — он отобьёт лавину до бэкенда.
- Ограничить память воркеров через cgroups/ulimit, чтобы ядро убивало распухший процесс быстро, а не валило весь хост. Это симптоматическое средство, но снижает радиус поражения.
Go: net/http и новый лимит числа значений
Если ваш сервис на Go — тема касается вас напрямую, и заодно она отлично иллюстрирует главный вывод статьи. HTTP/2-реализация Go к этой атаке устойчива: сервер закладывает около 32 байт накладных расходов на каждое значение заголовка и считает их против MaxHeaderBytes, так что лавина мусорных полей упирается в общий лимит размера. Это ровно наш тезис в цифрах — память уходит не на текст заголовков, а на служебный учёт под каждое поле.
А вот у net/http до недавнего времени не было отдельного лимита на число заголовков — только MaxHeaderBytes (суммарный размер). Получается «один параметр на два разных риска»: поднимаешь MaxHeaderBytes ради многокилобайтных SSO-кук — и заодно впускаешь тысячи мусорных однобайтовых полей. Слишком большой лимит открывает дорогу DoS, слишком маленький ломает легитимные запросы.
Это исправлено: proposal golang/go#79936 добавляет поле Server.MaxHeaderValueCount с дефолтом DefaultMaxHeaderValueCount = 500. Предложение принято (25 июня 2026) и идёт в Go 1.27 — со статусом release-blocker и одобренным freeze exception. Считаются именно значения, а не имена: иначе один заголовок с тьмой пустых значений обошёл бы лимит — та же логика, что с cookie-крамбами выше.
srv := &http.Server{
MaxHeaderBytes: 1 << 20, // суммарный размер заголовков (как и раньше)
MaxHeaderValueCount: 500, // число значений; 0 → DefaultMaxHeaderValueCount (500)
}Промежуточный вариант «только через GODEBUG=httpmaxheadervalues» (issue golang/go#80020) рассматривали, но закрыли как ненужный: решили не городить временный костыль, а сразу внести поле в Go 1.27. Практический вывод: если у вас Go-сервис с большим MaxHeaderBytes, обновление на 1.27 даст держать оба лимита раздельно и отбивать лавину мусорных заголовков, не ломая легитимный трафик.
Мета: как такое находят
Техническая часть на этом закончена — дальше о том, что делает этот кейс по-настоящему любопытным.
Обе половины атаки известны десять лет. HPACK-бомбу обсуждали ещё на заре HTTP/2; Slowloris-удержание старше самого HTTP/2. Спецификации предупреждают об обеих по отдельности. Но связку — «амплифицируй HPACK, потом запинь результат нулевым окном» — насколько известно авторам, до сих пор не собрал ни один человек. Хотя, как они сами признают, «once you see it» комбинация очевидна.
Собрал её Codex — ИИ-агент, которому дали прочитать исходники серверов. Он увидел, что две давно задокументированные механики компонуются, и вывел объединённую атаку. Формулировка авторов:
The attack was discovered by Codex, which chained two techniques known to humans for a decade… That combination is obvious once you see it, and yet as far as we can tell no human had put it together.
Здесь есть урок, выходящий за рамки конкретного CVE. Самые неприятные уязвимости часто живут не внутри компонента, а на стыке: каждый примитив по отдельности валиден, отревьюен и задокументирован — а опасна их комбинация. Человеку такие склейки даются тяжело: мы держим в голове один компонент за раз, разбиваем ревью по модулям, и «очевидное, когда увидишь» остаётся невидимым, пока кто-то не посмотрит на систему целиком. ИИ-агент, способный прочитать несколько кодовых баз разом и искать именно композиции, оказывается неожиданно силён ровно в этом классе задач.
Практический вывод для инженера: моделируя угрозы, смотрите на швы. «Каждая фича безопасна по отдельности» — не то же самое, что «система безопасна». А инструменты, которые умеют держать в поле зрения весь стек сразу, стоит держать по обе стороны баррикад — и в атаке, и в защите.
Итог
- HTTP/2 Bomb = HPACK-амплификация (1-байтовые индексные ссылки → полные поля) + удержание через нулевое flow-control окно с
WINDOW_UPDATEдля сброса таймаутов. По отдельности приёмы безобидны, вместе — remote DoS. - Уязвимы дефолты nginx, Apache, IIS, Envoy, Pingora; урон — десятки ГБ за секунды с одного клиента. Обход лимитов — через cookie-crumb splitting (RFC 9113 §8.2.3).
- Корневая защита у всех одна: лимит числа полей заголовков, считая cookie-крамбы. Патчи: nginx 1.29.8, Apache mod_http2 2.0.41 (CVE-2026-49975), Envoy 1.35.11/1.36.7. IIS и Pingora — митигации.
- Angie не подвержен:
max_headersесть с 1.8.0 (2024) — за полтора года до атаки. khorost.tech на Angie в безопасности. - Мета-урок: угроза живёт на стыке валидных примитивов, а склейку первым увидел ИИ. Моделируя угрозы, проверяйте швы — и используйте инструменты, видящие систему целиком.
Смежное на сайте: HAProxy L4 vs L7 и HTTP/2 — как устроен HTTP/2 на балансировщике и почему фронт с лимитами отбивает лавину до бэкенда; Bubblewrap: песочница для недоверенного кода — изоляция как рубеж обороны, снижающий радиус поражения.
Источники
- Calif — «Codex Discovered a Hidden HTTP/2 Bomb»: https://blog.calif.io/p/codex-discovered-a-hidden-http2-bomb
- Новость на linux.org.ru: https://www.linux.org.ru/news/security/18311265
- oss-sec, раскрытие: https://seclists.org/oss-sec/2026/q2/790
- Red Hat, бюллетень RHSB-2026-007 (CVE-2026-49975 — httpd, CVE-2026-47774 — Envoy): https://access.redhat.com/security/vulnerabilities/RHSB-2026-007
- Angie, история версий (1.8.0,
max_headers): https://en.angie.software/angie/docs/oss_changes/ - Go,
Server.MaxHeaderValueCount(принят, Go 1.27): https://github.com/golang/go/issues/79936 (старый связанный proposal — #62298; GODEBUG-вариант #80020 закрыт как not planned) - nginx,
ngx_http_v2_module; RFC 7541 (HPACK); RFC 9113 (HTTP/2, §6.9 flow control, §8.2.3 Cookie)
Комментарии