HTTP Request Smuggling
Разбираем, как расхождение в том, где заканчивается HTTP-запрос, между фронтендом и бэкендом создаёт уязвимость, и как её устранить.
HTTP Request Smuggling — уязвимость, возникающая, когда два сервера в цепочке (фронтенд-прокси и бэкенд) по-разному определяют границу тела запроса и теряют синхронизацию потока.
В типичной инфраструктуре запрос проходит через несколько узлов: балансировщик или обратный прокси (фронтенд) принимает соединение и пересылает запрос приложению (бэкенд). Чтобы понимать, где кончается один запрос и начинается следующий в одном TCP-соединении, серверы смотрят на длину тела. Длину можно задать двумя способами — заголовком Content-Length (точное число байт) или механизмом Transfer-Encoding: chunked (тело передаётся порциями). Уязвимость рождается, когда фронтенд и бэкенд по-разному решают, какой способ применять, и расходятся в том, где проходит граница. Это относится к ошибкам корректной обработки протокола.
Тема чисто инфраструктурная и тонкая, поэтому особенно важно подчеркнуть: исследовать рассинхронизацию допустимо только на собственном стенде. Воздействие на чужую инфраструктуру, нарушающее её работу, подпадает под статьи 272 и 274 УК РФ.
Зачем это знать защитнику
Smuggling опасен тем, что эксплуатирует не баг в одном сервере, а несогласованность между двумя корректными по отдельности серверами. Последствия серьёзные: «протащенная» часть запроса может приклеиться к запросу другого пользователя, что ведёт к обходу правил доступа фронтенда, отравлению кеша и перехвату чужих ответов. Инженер, который понимает механику, правильно выбирает и настраивает прокси и не оставляет в цепочке узлы с разным пониманием границ запроса.
Как возникает уязвимость
Спецификация HTTP/1.1 говорит: если в запросе одновременно присутствуют и Content-Length, и Transfer-Encoding: chunked, приоритет у Transfer-Encoding, а такой запрос вообще считается подозрительным. Проблема в том, что разные реализации исторически вели себя по-разному: один сервер ориентируется на Content-Length, другой — на Transfer-Encoding. Схематично «спорный» запрос с двумя указателями длины:
POST / HTTP/1.1
Host: lab.local
Content-Length: 6
Transfer-Encoding: chunked
0
(дальше байты, которые один сервер считает концом тела, а другой — началом нового запроса)
Если фронтенд доверяет одному заголовку, а бэкенд — другому, они расходятся в том, где кончается тело. Часть данных, которую фронтенд счёл «телом текущего запроса», бэкенд воспринимает как «начало следующего запроса» (или наоборот). Эта повисшая часть и есть «контрабанда»: она встраивается в обработку так, как не предполагал фронтенд. Конкретные рабочие комбинации заголовков под живые цели мы не приводим — нам важен принцип расхождения границ.
Как это работает под капотом
Ключ в том, что HTTP/1.1 позволяет держать несколько запросов в одном соединении друг за другом (keep-alive). Сервер просто читает поток байтов и нарезает его на запросы по объявленной длине тела. Если два сервера нарезают один и тот же поток по-разному, граница «съезжает». Тогда хвост одного запроса прилипает к голове следующего. Поскольку следующий запрос в этом соединении может принадлежать другому пользователю, атакующий влияет на чужой запрос, не имея к нему прямого доступа. Вся уязвимость — в неоднозначности «где конец тела», помноженной на повторное использование соединения.
На уровне принципа расхождение бывает двух зеркальных видов. В одном фронтенд считает запрос законченным раньше, чем бэкенд, и «лишний» хвост остаётся в соединении ждать следующего запроса. В другом — наоборот, бэкенд считает, что тело ещё продолжается, и «дочитывает» в него начало следующего запроса. Оба случая сводятся к одному корню: два узла разошлись в подсчёте длины. Понимание этой симметрии помогает при настройке цепочки — задача не «угадать правильный заголовок», а гарантировать, что все узлы считают длину одинаково и однозначно.
Как защититься
1. Единый и строгий парсер на всех узлах
Главная мера — чтобы все серверы в цепочке одинаково и строго трактовали границы запроса. Запрос, в котором одновременно есть Content-Length и Transfer-Encoding, должен отклоняться, а не «как-нибудь» интерпретироваться. Современные прокси и веб-серверы при актуальных версиях так и делают — поэтому критично держать инфраструктуру обновлённой.
# Принцип нормализации на фронтенде (псевдо-политика)
ЕСЛИ в запросе есть и Content-Length, и Transfer-Encoding -> отклонить (400)
ЕСЛИ Transfer-Encoding некорректный/неполный -> отклонить (400)
ИНАЧЕ -> переслать на бэкенд в одном, нормализованном виде
2. Нормализуйте запросы перед пересылкой
Фронтенд должен пересобирать исходящий к бэкенду запрос в каноничном виде: один способ указания длины, никаких дублирующихся или конфликтующих заголовков. Так бэкенд получает однозначный поток и не может разойтись с фронтендом.
3. Используйте HTTP/2 между фронтендом и бэкендом
HTTP/2 определяет длину сообщения на уровне фреймов протокола, а не текстовых заголовков Content-Length/Transfer-Encoding. Это убирает саму почву для классической рассинхронизации. Сквозной HTTP/2 (и аккуратная, валидирующая конвертация, если на каком-то участке остаётся HTTP/1.1) заметно снижает риск.
4. Ограничивайте повторное использование соединений и мониторьте аномалии
Где это приемлемо, ограничьте переиспользование соединения между прокси и бэкендом, следите за странностями в логах (необъяснимые «склейки» запросов, всплески ошибок парсинга) и тестируйте свою цепочку специализированными инструментами в лаборатории.
Итоги
- Request smuggling — это рассинхронизация фронтенда и бэкенда по поводу границы тела запроса.
- Источник — конфликт
Content-LengthиTransfer-Encodingпри разной их трактовке серверами, помноженный на keep-alive. - Защита: единый строгий парсер (отклонять двусмысленные запросы), нормализация перед пересылкой, HTTP/2 между узлами.
- Держите все узлы цепочки на актуальных версиях — современные парсеры закрывают исторические расхождения.
- Любые проверки — только на своей инфраструктуре; нарушение работы чужих систем наказуемо (УК РФ ст. 272/274).