Перехват трафика HTTP/3: почему прокси слепнет на QUIC

Перехват трафика HTTP/3 не работает в привычных прокси вроде Burp Suite: протокол идёт по QUIC внутри UDP, и TCP-прокси его просто не видит. Команда InterceptSuite 26 октября 2026 года опубликовала практический разбор: куда пропадают запросы, как вернуть их в прокси и почему заголовки даже после расшифровки не похожи на текст. Для тех, кто тестирует веб-приложения и API, речь идёт о полноте проверки.
Коротко
- HTTP/3 работает поверх QUIC внутри UDP, поэтому TCP-прокси вроде Burp Suite его не видят.
- Браузер обычно делает первый запрос по TCP и переходит на QUIC после Alt-Svc или DNS-записи HTTPS, так что история прокси получается рваной.
- В лаборатории InterceptSuite один HTTP/3-запрос дал десять строк истории, и большая часть из них — служебные потоки.
- curl отправляет заголовки QPACK литералами (ёмкость таблицы 0), Edge — ссылками на динамическую таблицу на 64 КБ, и читать их сложнее.
- ECH и недоверие Chromium к пользовательскому CA на QUIC ломают наивный перехват. Обходы допустимы только в отдельном тестовом профиле.
Почему обычный прокси не видит HTTP/3
Обычный HTTP-прокси не видит HTTP/3, потому что этот протокол не ходит по TCP, а Burp Suite и его аналоги работают именно с TCP-соединениями. С этого начинается разбор InterceptSuite. Это методика тестирования, а не сообщение об уязвимости: CVE и группировок в нём нет.
Причина в архитектуре. HTTP/3 описан в RFC 9114 и работает поверх QUIC (RFC 9000), а QUIC упаковывает данные в UDP-датаграммы. TLS 1.3 здесь не надстраивается отдельным слоем, как в HTTPS на HTTP/1.1 или HTTP/2, а встроен прямо в рукопожатие QUIC (RFC 9001). Привычной цепочки «открыли TCP, договорились о TLS, пошли запросы» больше нет.
Перехватывающему прокси здесь не на что опереться. Burp встаёт посередине TCP-соединения, терминирует TLS своим сертификатом и видит строку запроса и блок заголовков. С QUIC так не выйдет: UDP-датаграммы идут мимо, а текстовой строки «метод — путь — версия» там нет даже после расшифровки.
С HTTP/2 разница принципиальная. HTTP/2 тоже бинарный и тоже мультиплексирует потоки, но работает по TCP/443, и после снятия TLS его разбирает любой современный прокси. HTTP/3 обычно занимает UDP/443, а в согласовании протоколов (ALPN) обозначается как h3.
Главный практический вывод: тишина в истории прокси не доказывает, что трафика не было. Возможно, клиент договорился с сервером на HTTP/3 и ушёл в UDP, а инструмент смотрит в другую сторону. Чтобы прочитать такой трафик, прокси должен уметь пересылать UDP и сам вставать посередине QUIC-соединения.
Как клиент узнаёт, что сервер говорит на HTTP/3?
Клиент узнаёт про HTTP/3 из заголовка Alt-Svc в ответе сервера или из DNS-записи типа HTTPS (RFC 9460). Сервер должен объявить поддержку, клиент — её заметить. Сам собой переход не случается.
С Alt-Svc всё происходит так. Браузер впервые заходит на сайт по TCP, через HTTP/2 или HTTP/1.1. В ответе приходит заголовок: тот же сервис доступен по h3 на таком-то порту. Браузер запоминает подсказку и следующие запросы уже пытается отправить через QUIC. DNS-запись HTTPS сообщает то же самое ещё до первого соединения, прямо при разрешении имени.
Для пентестера здесь ловушка. Первый запрос сессии часто честно проходит через Burp, тестировщик видит главную страницу и решает, что перехват работает. Потом браузер переходит на QUIC, и вызовы API, загрузки файлов, запросы из JavaScript тихо уходят мимо. История получается рваной: одни эндпоинты видны, другие нет, и закономерность неочевидна.
У этой механики есть и обратная сторона. Если заблокировать UDP/443, HTTPS обычно не ломается: клиент откатывается на HTTP/2 или HTTP/1.1 поверх TCP/443, если сервер их поддерживает. Сайт открывается, просто по другому протоколу. Так легко проверить, виноват ли QUIC: если после фильтрации UDP пропавшие запросы появились в прокси, ответ найден.
Для теста такой откат остаётся компромиссом. Вы проверяете HTTP/2-версию приложения, а не ту, что обслуживает большинство пользователей современных браузеров. Если HTTP/3 обрабатывает другой фронтенд или узел CDN с иной конфигурацией, разница в поведении может оказаться заметной.
Как завести QUIC в прокси, если CONNECT и SOCKS5 не помогают
Браузерный QUIC не проходит через привычные механизмы проксирования, потому что почти все они рассчитаны на TCP. HTTP CONNECT даже в HTTP/3 строит туннель к TCP-соединению (RFC 9114, раздел 4.4). Для UDP есть отдельный метод CONNECT-UDP (RFC 9298), но большинство приложений и инструментов им не пользуется.
На бумаге выход даёт SOCKS5: в RFC 1928 есть команда UDP ASSOCIATE с кодом 03. Клиент открывает TCP-соединение с прокси, проходит приветствие и отправляет эту команду. Прокси отвечает адресом и портом узла, который будет пересылать UDP-датаграммы. Каждая датаграмма идёт туда с SOCKS-заголовком: 10 байт для IPv4, 22 для IPv6, переменная длина для доменного имени.
Ассоциация живёт, пока живёт управляющее TCP-соединение (RFC 1928, раздел 6). Закрылось оно — пересылка прекращается, и об этом легко забыть, когда пишешь собственный клиент. У InterceptSuite SOCKS5 по TCP слушает 127.0.0.1:4444, а UDP-датаграммы принимает на 127.0.0.1:4445.
Беда в том, что Chromium этой возможностью не пользуется. В документации Chromium по прокси прямо сказано: SOCKSv5 применяется только для TCP-запросов и UDP не пересылает. Прописать браузеру SOCKS-прокси и ждать в нём QUIC бесполезно: путь через QUIC вы при этом не тестируете.
InterceptSuite описывает два рабочих пути:
- Приложение само говорит SOCKS5 UDP. Подходит для собственных клиентов и библиотек. В лаборатории авторы написали клиент на Python и aioquic с прослойкой, которая добавляет SOCKS-заголовок к каждой QUIC-датаграмме и снимает его при приёме. Сам aioquic про SOCKS5 ничего не знает.
- Трафик программы перенаправляют снаружи. Приложение о прокси не знает, его трафик перенаправляет внешний компонент. У InterceptSuite это ProxyBridge, который работает самостоятельно и встроен во вкладку Redirect.
Настройка перенаправления короткая. В разделе Proxy → Redirect добавляют правило с именем программы, например curl.exe, и запускают перенаправление: TCP- и UDP-трафик программы идёт через прокси без правки её настроек. Правило сужают по хостам и портам, срабатывает первое подходящее, программы без правил не трогаются. CA InterceptSuite должен быть в доверенных.
Полезно понимать и устройство самого инструмента. Ядро InterceptSuite работает с сырыми байтами TCP, TLS, UDP, DTLS и QUIC и HTTP-парсера не содержит. Читаемый текст из HTTP/3 восстанавливает отдельное открытое расширение через API самой программы. Если расширение не справилось с QPACK, вы увидите байты, а не заголовки.
Почему один запрос HTTP/3 превращается в десять строк истории
Прокси, который разбирает QUIC, видит вместо пары «запрос — ответ» набор потоков, и большинство из них служебные. В лаборатории InterceptSuite один HTTP/3-запрос оставил в истории десять строк, а не две. Каждая строка — порция данных из одного QUIC-потока.
Стенд простой. Сервер HTTP/3 на 127.0.0.1:4433 с самоподписанным сертификатом возвращает в JSON то, что получил: метод, путь, один нестандартный заголовок и тело. Клиент отправил POST /login с заголовками authorization, cookie и x-demo. В колонке хоста прокси показал h3.lab.test — имя сервера из рукопожатия, в колонке ALPN — h3.
Откуда столько строк, видно из нумерации потоков. Смысл каждого потока задают два младших бита идентификатора (RFC 9000, раздел 2.1). Один бит показывает, кто открыл поток — клиент или сервер. Второй — двунаправленный он или однонаправленный. В HTTP/3 каждый двунаправленный поток, открытый клиентом (номера 0, 4, 8 и далее), несёт ровно одну пару запрос/ответ.
Однонаправленные потоки отданы под служебную работу. Их тип определяет первый байт: 0x00 — управляющий поток, 0x02 — поток кодировщика QPACK, 0x03 — поток декодировщика QPACK (RFC 9114, раздел 6.2, и RFC 9204, раздел 4.2). Каждая сторона открывает свой комплект.
Среди строк лабораторной истории оказались:
- однобайтовые записи с типами потоков кодировщика и декодировщика QPACK от каждой из сторон;
- управляющие потоки: 12 байт от сервера и 15 байт от клиента, клиентский длиннее на три байта из-за параметра MAX_PUSH_ID;
- трёхбайтовая инструкция QPACK, которая задаёт ёмкость динамической таблицы в 4096 байт;
- 144 байта собственно запроса клиента: фрейм HEADERS, за ним DATA;
- 171 байт ответа сервера.
Отсюда правило для отчётов, которое полезно закрепить в команде: датаграммы, QUIC-потоки и HTTP-запросы считаются отдельно. Десять строк в истории не значат десять обращений к приложению. Если считать запросы по строкам прокси, метрики покрытия раздуваются в разы.
Собрать картину помогает язык фильтров истории: по connection_id, размеру, направлению, протоколу, хосту, ALPN и признаку правки. Фильтр по connection ID сводит десять строк одного запроса в одно соединение.
Почему заголовки HTTP/3 нельзя прочитать как текст?
Заголовки HTTP/3 сжаты механизмом QPACK (RFC 9204) — родственником HPACK из HTTP/2, переделанным под QUIC. Даже после расшифровки во фрейме HEADERS лежат не строки вида «user-agent: …», а закодированные инструкции.
QPACK работает с двумя таблицами. Статическая зашита в стандарт и содержит частые заголовки и значения. Динамическую стороны наполняют по ходу соединения: один раз передали длинную cookie, дальше ссылаются на неё по индексу. Обновления таблицы идут по потоку кодировщика, подтверждения — по потоку декодировщика. Отсюда и служебные строки в истории прокси.
По умолчанию динамическая таблица выключена: по RFC 9204 обе настройки QPACK равны нулю. Включает её сторона, которая объявляет ненулевые значения во фрейме SETTINGS. Лабораторный сервер на aioquic сделал это явно: ёмкость 4096 байт, до 16 заблокированных потоков. Заодно он прислал ENABLE_CONNECT_PROTOCOL и зарезервированную GREASE-настройку, которая приучает реализации не падать на незнакомых параметрах.
Самое показательное в разборе — сравнение двух клиентов:
| Параметр | curl 8.23.0 | Microsoft Edge |
|---|---|---|
| QPACK_MAX_TABLE_CAPACITY | 0 | 64 КБ |
| Заблокированные потоки | — | до 100 |
| Как уходят заголовки | всегда литералами | после первого запроса — ссылками на таблицу |
| Что нужно прокси для чтения | литералы читаются напрямую | нужно состояние таблицы и её индексы |
curl объявляет ёмкость таблицы ноль и динамической таблицей не пользуется вовсе. Каждый заголовок уходит литералом, его легко прочитать и поправить. Edge предлагает таблицу на 64 КБ, и после первого запроса заголовки идут ссылками на записи таблицы, а не строками.
Чтобы прочитать поздний запрос Edge, декодеру нужно актуальное состояние таблицы и все обновления с потока кодировщика. Изолированный фрейм HEADERS без этого контекста в осмысленный вид не превратить, даже когда TLS уже снят. Оборвалась запись соединения или потерялись служебные потоки — поздние заголовки становятся набором индексов.
С правкой на лету ещё сложнее: изменение заголовка требует согласованного изменения таблицы, иначе сервер получит рассинхронизированный контекст. Практический вывод: сохраняйте соединение целиком, с начала рукопожатия и со всеми служебными потоками, а не выгружайте один интересный HEADERS.
Что ломает перехват QUIC в браузере?
Перехват браузерного QUIC ломают два механизма: Encrypted Client Hello и отказ Chromium работать по QUIC с сертификатом от пользовательского CA. Оба делают ровно то, для чего задуманы, — защищают пользователей. Тестировщику их приходится учитывать, а не чинить.
ECH прячет настоящее имя сайта в зашифрованной части рукопожатия. Снаружи видно только публичное имя фронтенда, обычно CDN. Прокси не знает, к какому сайту на самом деле идёт клиент, выпускает сертификат на имя CDN, и браузер его отвергает: имя не совпадает.
Второе ограничение тоньше. Chromium не поднимает QUIC, если цепочка сертификата ведёт к корневому CA, который установил пользователь, даже когда хранилище Windows этому CA доверяет. По TCP такой сертификат пройдёт, по QUIC — нет. Браузер молча откатывается на TCP, и тестировщик снова проверяет не тот протокол.
Без ECH пассивный наблюдатель видит больше, чем кажется. Первые пакеты соединения, Initial, защищены ключами, которые любой вычисляет из connection ID (RFC 9001, раздел 5), поэтому имя сервера из рукопожатия читается без перехвата. Всё после рукопожатия шифруется ключами TLS 1.3. Прокси, который хочет читать HTTP/3, должен сам стать конечной точкой для клиента и для сервера.
Авторы перечисляют флаги запуска Edge, которыми тестировщики отключают ECH и DNS-записи HTTPS, принудительно включают QUIC и доверяют CA прокси по хешу его открытого ключа. Мы их сознательно не воспроизводим: это настройка стенда, а не рекомендация. Держите такие флаги только в отдельном тестовом профиле и следите, чтобы они не попали в ярлыки и политики рабочих браузеров.
С curl проще. Авторы отправили запрос к cloudflare-quic.com в режиме —http3-only, чтобы curl не откатился на TCP, и получили HTTP/3 200. Затем включили перехват и заменили user-agent curl/8.23.0 на edited-by-proxy. Тестовый сайт возвращает то, что получил, и в ответе стояло uag=edited-by-proxy: сервер принял изменённый заголовок, а curl ничего не заметил.
Последняя оговорка касается собственных клиентов. Лабораторный клиент InterceptSuite отключает проверку сертификата, и авторы прямо называют это допустимым только в лаборатории. Настоящее приложение должно доверять CA прокси, иначе на подменном сертификате оно закроет соединение. Тестируете приложение с собственным HTTP/3-стеком — закладывайте время на корректное добавление тестового CA, а не на отключение проверок в сборке.
Как перехват трафика HTTP/3 меняет план пентеста
Перехват трафика HTTP/3 нужно прописывать в плане теста явно, как отдельный пункт покрытия. Если не проверить, на каком протоколе приложение реально работает, часть поверхности атаки останется непроверенной без единого следа в отчёте.
Из практики: при тестировании безопасности веб-приложений мы первым делом смотрим, отдаёт ли сервер Alt-Svc или DNS-запись HTTPS с h3. Если отдаёт, фиксируем в отчёте, какой протокол проверяли и что сделали с откатом. Типичная находка — разная конфигурация на TCP- и QUIC-фронтендах: заголовки безопасности, лимиты или правила WAF настроены только для одного из них.
Что проверить у себя
- Клиент. Выполните curl -V и посмотрите, есть ли HTTP3 в списке возможностей. Встроенный в Windows curl 8.21.0 HTTP/3 не поддерживает. А вот сборка 8.23.0, установленная через winget (пакет cURL.cURL), показывает HTTP3, ngtcp2 и nghttp3.
- Сервер. curl —http3-only -sS -D — к адресу вашего сервиса покажет, отвечает ли он по HTTP/3.
- Сеть. tcpdump -ni any ‘udp port 443’ на тестовом хосте или шлюзе показывает, есть ли QUIC-трафик вообще.
- Браузер. Страница edge://version или chrome://version показывает фактические флаги запуска, а net-export снимает журнал сетевых событий тестового профиля.
- Отчёт. Датаграммы, QUIC-потоки и HTTP-запросы считайте раздельно, сохраняйте полные соединения со служебными потоками.
С мобильными клиентами то же самое: сетевые библиотеки всё чаще включают HTTP/3 по умолчанию, и при тестировании мобильных приложений трафик может уходить в UDP мимо настроенного прокси. Для бэкенда логика та же: при тестировании безопасности API проверяйте, одинаково ли шлюз обрабатывает запросы по HTTP/2 и HTTP/3.
Что увидит SOC
Сетевому мониторингу HTTP/3 оставляет меньше, чем HTTP/2. QUIC шифрует не только содержимое, но и большую часть транспортной служебной информации, включая номера пакетов и многие управляющие фреймы. Пассивному наблюдателю остаются метаданные, размеры, тайминги и поля Initial-пакетов.
QUIC держит соединение по connection ID, а не по адресам и портам. Клиент переключился с Wi-Fi на LTE или получил от NAT новый порт — соединение продолжается. Журналы, которые склеивают сессии по пятёрке «адреса, порты, протокол», получают разрывы: одна логическая сессия выглядит как несколько несвязанных сессий. Сверяйте журнал прокси, PCAP и журнал приложения по времени и connection ID.
Разработчикам полезно помнить ещё два свойства QUIC:
- Лимит против усиления. Пока адрес клиента не подтверждён, сервер отправляет не больше трёх байт на каждый полученный. Это встроенная защита от усиления атак (amplification), когда сервер QUIC не отражает трафик на подставной адрес жертвы.
- 0-RTT. Клиент может отправить ранние данные без полного рукопожатия, но атакующий способен воспроизвести их повторно. Платёж, смену пароля или любой POST, меняющий состояние, нужно защищать от повтора на уровне приложения.
Для межсетевых экранов вывод простой: разрешённый TCP/443 не значит разрешённый QUIC. Правила для UDP/443 должны появляться осознанно, а откат на HTTP/2 — работать там, где UDP закрыт.
Чего разбор InterceptSuite не покрывает
Это описание одной методики на одном стенде. В статье два клиента — curl и Microsoft Edge на движке Chromium. Firefox, Safari и мобильные сетевые стеки не рассмотрены, а их поведение с пользовательскими CA и ECH может отличаться. Проверяйте каждый клиент из вашего плана отдельно.
Текст написан разработчиком инструмента и демонстрирует собственный продукт. Архитектурные факты опираются на RFC и проверяются независимо, удобство конкретного инструмента — вопрос вашей оценки. Учётные данные в лабораторном запросе — пользователь alice, пароль hunter2, демонстрационный токен — выдуманы для примера и утечкой не служат.
Частые вопросы
Почему Burp Suite не перехватывает HTTP/3?
HTTP/3 идёт по QUIC внутри UDP, а Burp работает с TCP-соединениями. Если браузер перешёл на HTTP/3, его запросы проходят мимо прокси и не оставляют записей в истории.
Как браузер узнаёт, что сервер поддерживает HTTP/3?
Из заголовка Alt-Svc в ответе сервера или из DNS-записи типа HTTPS (RFC 9460). Поэтому первый запрос часто идёт по TCP, а последующие — уже по QUIC.
Что такое QPACK и чем он отличается от HPACK?
QPACK — механизм сжатия заголовков в HTTP/3 (RFC 9204), переработанный HPACK из HTTP/2. Обновления динамической таблицы он передаёт по отдельным однонаправленным потокам, чтобы не блокировать независимые потоки QUIC.
Мешает ли Encrypted Client Hello перехвату HTTPS?
Да. ECH скрывает настоящее имя сайта, прокси выпускает сертификат на публичное имя CDN, и браузер его отвергает. На стенде ECH отключают только в отдельном тестовом профиле браузера.
Можно ли заставить браузер ходить по HTTP/2 вместо HTTP/3?
Да: если заблокировать UDP/443, клиент обычно откатывается на HTTP/2 или HTTP/1.1 поверх TCP. Но тогда вы проверяете HTTP/2-версию приложения, а не ту, что обслуживает браузеры с поддержкой HTTP/3.
Если не уверены, что ваш пентест видит весь трафик приложения, включая HTTP/3, команда HirexTech проведёт тестирование на проникновение с явной проверкой QUIC и отката на HTTP/2.