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).
Проверьте себя
1. Что лежит в основе HTTP Request Smuggling?
AСлишком длинные URL в запросах
BРасхождение между фронтендом и бэкендом в том, где заканчивается тело запроса
CОтсутствие шифрования TLS на бэкенде
DИспользование слишком старой версии HTML
2. Как строгий парсер на всех узлах помогает против smuggling?
AОн сжимает тело запроса, чтобы оно занимало меньше места
BОн отклоняет двусмысленные запросы (например, с одновременно Content-Length и Transfer-Encoding), не давая серверам разойтись
CОн кеширует ответы, чтобы ускорить сайт
DОн шифрует заголовки запроса
3. Почему сквозной HTTP/2 снижает риск request smuggling?
AHTTP/2 полностью отключает кеширование
BВ HTTP/2 длина сообщения задаётся на уровне фреймов протокола, а не текстовыми заголовками Content-Length/Transfer-Encoding
CHTTP/2 запрещает POST-запросы
DHTTP/2 не использует TCP-соединения