HTTP Parameter Pollution выглядит почти смешно: в запрос добавляют два одинаковых параметра, например ?role=user&role=admin, и ждут, какая часть системы какой вариант выберет. Но именно простота делает прием неприятным. Балансировщик, WAF, веб-фреймворк, самописный код, внутренний API и журналирование могут по-разному читать один и тот же запрос.
Проблема не в том, что HTTP «сломался». Проблема в договоренности, которой часто нет. Один слой берет первое значение, другой последнее, третий склеивает все значения в массив, четвертый превращает значения в строку через запятую. На бумаге запрос один. В реальной системе запрос распадается на несколько разных смыслов.
Что такое HTTP Parameter Pollution простыми словами
HTTP Parameter Pollution, или HPP, это ситуация, когда злоумышленник передает несколько параметров с одним именем в URL, форме или теле запроса с типом application/x-www-form-urlencoded. OWASP описывает такую проверку как тест реакции приложения на несколько HTTP-параметров с одинаковым именем, например когда username встречается в запросе дважды. Подробно методика разобрана в OWASP.
Базовый пример выглядит так:
GET /profile?user=ivan&user=admin HTTP/1.1
Host: example.local
Если модуль авторизации проверяет первое значение ivan, а бизнес-логика потом берет последнее значение admin, проверка и действие расходятся. В таком расхождении и живет уязвимость.
На SecurityLab еще много лет назад описывали HPP как вставку дополнительных ограничителей запроса или параметров с тем же именем для обхода ограничений безопасности. Формулировка старая, но сама идея никуда не исчезла. Современные API, микросервисы, прокси и шлюзы только добавили слоев, где поведение может разъехаться.
Почему повтор параметра не всегда ошибка
Повторяющиеся параметры не обязательно вредны. Иногда повтор нужен по смыслу:
GET /search?tag=linux&tag=security&tag=http HTTP/1.1
Такой запрос может означать поиск по нескольким тегам. В HTML-формах несколько отмеченных чекбоксов тоже часто отправляют несколько значений с одним именем. Проблема начинается там, где разработчик не решил заранее, какие параметры могут повторяться, а какие должны приходить строго один раз.
Если параметр tag повторяется легально, приложение должно явно читать набор значений. Если параметр role, price, redirect_uri, account_id, is_admin или amount повторяется, чаще всего запрос надо отклонять, а не пытаться угадывать намерение клиента.
Где ломается логика: один запрос, несколько трактовок
Типовая цепочка обработки запроса редко состоит из одного приложения. Запрос проходит через CDN, обратный прокси, WAF, балансировщик, сервер приложения, фреймворк, middleware, контроллер, клиент внутреннего API, иногда еще через очередь или сервис авторизации. Каждый слой может нормализовать параметры по-своему.
| Слой | Что может увидеть | Где риск |
|---|---|---|
| WAF | Первое значение параметра | Фильтр пропускает запрос, потому что опасное значение стоит вторым |
| Фреймворк | Последнее значение параметра | Контроллер выполняет действие с другим значением, чем проверял WAF |
| Внутренний API | Массив значений | Сервис выбирает элемент массива не так, как внешний слой |
| Кэш | Нормализованную строку запроса | Кэш-ключ не совпадает с реальным смыслом запроса |
| Логи | Обрезанную или уже разобранную версию | Инцидент потом трудно расследовать, потому что сырой запрос потерян |
Уязвимость часто возникает не в одном конкретном модуле, а на стыке. Команда безопасности настраивает правило на WAF, разработчики пишут проверку в контроллере, соседняя команда поднимает внутренний сервис, а никто не фиксирует общий контракт обработки повторяющихся параметров.
Классический сценарий: фильтр смотрит одно, приложение делает другое
Представим условную страницу перевода денег:
POST /transfer HTTP/1.1
Host: bank.local
Content-Type: application/x-www-form-urlencoded
to=alice&amount=100&amount=0
Один компонент может проверить amount=100 и решить, что лимит не нарушен. Другой компонент может взять amount=0 или склеить значения в строку 100,0. В реальном приложении последствия зависят от бизнес-логики, типов данных и дальнейших проверок. Иногда запрос просто упадет с ошибкой. Иногда ошибка превратится в обход валидации.
Похожий прием встречается в авторизации:
GET /admin/report?user_id=42&user_id=1 HTTP/1.1
Если проверка доступа идет по первому user_id, а выборка отчета по второму, возникает разрыв между «кого проверили» и «чьи данные отдали». Такой пример не надо воспринимать как готовый рецепт взлома конкретного сайта. В нормальной системе проверка прав не должна зависеть от пользовательского параметра без серверного контекста.
Server-side Parameter Pollution в современных API
Отдельный неприятный вариант появляется, когда внешний параметр встраивают во внутренний серверный запрос. PortSwigger описывает server-side parameter pollution как ситуацию, когда сайт добавляет пользовательский ввод в запрос к внутреннему API без достаточного кодирования. Тогда атакующий может вмешаться в параметры внутреннего запроса.
Условная схема:
GET /forgot-password?email=ivan@example.local
Внешний сервер внутри может собрать запрос так:
GET /internal/reset?email=ivan@example.local&token=generated
Если значение email не кодируют, атакующий может попытаться передать строку с дополнительным разделителем:
email=ivan@example.local%26token=empty
После неаккуратной сборки внутренний сервис может получить уже не одно поле email, а новый параметр token. Конкретный эффект зависит от внутреннего API. Где-то атака не сработает вообще. Где-то получится изменить поведение восстановления пароля, поиска пользователя, фильтрации заказов или административного действия.
Почему WAF не спасает сам по себе
WAF полезен, но HPP как раз показывает слабое место фильтрации на периметре. Фильтр видит HTTP-запрос не глазами приложения, а глазами собственного парсера. Если WAF применяет правило к первому значению параметра, а приложение берет последнее, правило может дать ложное чувство защиты.
Похожая проблема возникает при сигнатурах. Атакующий может разделить полезную нагрузку между несколькими одноименными параметрами или спрятать опасную часть там, где фильтр не ожидает увидеть рабочее значение. Хорошие WAF умеют нормализовать запросы и ловить аномалии, но политика приложения все равно должна быть жестче: повтор критичного параметра лучше отклонять до бизнес-логики.
Где искать HPP при проверке приложения
Начинать стоит не с экзотики, а с параметров, которые влияют на деньги, права, идентификаторы, маршруты и состояние:
user_id,account,client_id,tenant,org_id;role,scope,permission,is_admin;price,amount,currency,discount;redirect,redirect_uri,return_url,next;sort,filter,status, если параметры влияют на выборку данных.
Проверка простая по форме, но требует аккуратности. Для каждого параметра надо отправить запрос с повтором и посмотреть, какое значение реально использует приложение. Затем надо сравнить поведение разных входов: браузерной формы, API, мобильного клиента, внутреннего шлюза, кэша и журнала событий.
GET /orders?status=paid&status=cancelled HTTP/1.1
Host: example.local
Если ответ совпадает с status=paid, приложение, вероятно, берет первое значение. Если совпадает с status=cancelled, приложение берет последнее. Если вернулись заказы обоих типов, параметр стал массивом. Если прилетела ошибка 400, поведение может быть безопасным, если отказ действительно системный и одинаковый для всех критичных параметров.
Нормальная защита от HPP начинается не с черного списка опасных символов, а с четкого контракта: какие параметры могут повторяться, какие не могут, где запрос отклоняется и какая нормализованная форма уходит дальше по системе.
Почему «просто взять последнее значение» плохая идея
Иногда разработчики решают: пусть последнее значение побеждает. Такой подход удобен, но опасен. Победа последнего значения не устраняет расхождение между слоями. WAF, кэш или внутренний сервис все равно могут жить по другим правилам.
Победа первого значения ничем не лучше. Склеивание в массив тоже не спасает, если бизнес-логика потом не проверяет размер массива. В безопасности важен не выбранный вариант сам по себе, а единый вариант для всей цепочки. Для критичных параметров самый надежный вариант обычно звучит жестко: повтор запрещен, запрос получает 400, событие попадает в журнал.
Как защищаться без иллюзий
Защита должна быть скучной и последовательной. На входе приложение должно разобрать параметры один раз, привести запрос к канонической форме и дальше передавать уже проверенную структуру, а не сырой URL. Если параметр не должен повторяться, повтор надо считать ошибкой клиента.
Пример политики:
- id, amount, role, redirect_uri принимаются только один раз
- tag, category, ids могут повторяться только как список
- неизвестные параметры отклоняются или игнорируются по явному правилу
- внутренние запросы собираются через безопасный конструктор URL, а не конкатенацией строк
В коде надо избегать ситуаций, где один модуль вызывает условный get("amount"), а другой getAll("amount"). Такой разнобой рано или поздно даст странное поведение. Лучше сделать один слой нормализации и запретить остальному коду читать сырые параметры напрямую.
Для внутренних API полезно подписывать не исходную строку запроса, а каноническое представление параметров. Тогда a=1&b=2 и b=2&a=1 не превращаются в разные сущности без причины, а a=1&a=2 получает заранее определенный статус. Если параметр должен быть списком, порядок и формат списка тоже фиксируются.
Что добавить в тесты
HPP хорошо ловится автоматическими и ручными проверками, если тесты не ограничиваются «правильными» запросами. Для каждого критичного обработчика стоит добавить негативные случаи:
- повтор одиночного параметра с одинаковыми значениями;
- повтор одиночного параметра с разными значениями;
- повтор параметра в URL и теле формы одновременно;
- параметр с закодированным разделителем
%26; - параметр, который разрешен как список, но приходит в неожиданном формате.
Хороший признак зрелости: тесты проверяют не только ответ приложения, но и то, что ушло во внутренний сервис. Многие ошибки HPP не видны на первом HTTP-ответе. Разрыв появляется дальше, в запросе к микросервису, в кэш-ключе, в событии аудита или в задаче очереди.
Parameter Pollution и похожие проблемы
HPP часто путают с другими классами ошибок. Дублирующиеся ключи в JSON похожи по духу, но технически относятся к другой области: разные JSON-парсеры тоже могут по-разному обрабатывать повтор ключа. HTTP Request Smuggling тоже живет на расхождении парсеров, но касается границ HTTP-запросов, заголовков и длины тела. Parameter Pollution проще: спор идет вокруг значения конкретного параметра.
Еще рядом стоит Parameter Tampering, где атакующий просто меняет значение параметра. Например, было price=100, стало price=1. При HPP меняется не только значение, но и трактовка: price=100&price=1 может пройти разные проверки по-разному.
Чеклист для разработки и пентеста
| Вопрос | Здоровый ответ |
|---|---|
| Есть ли список параметров, которым разрешен повтор? | Да, список короткий и привязан к конкретным обработчикам |
| Что происходит при повторе критичного параметра? | Запрос отклоняется с 400 до бизнес-логики |
| Кто нормализует параметры? | Один общий слой, а не каждый контроллер отдельно |
| Как собираются внутренние URL? | Через библиотеку кодирования параметров, без склейки строк |
| Что хранится в логах? | Сырой запрос и нормализованная форма с пометкой об отклонении |
| Проверяет ли WAF дубли параметров? | Да, но приложение не полагается только на WAF |
Где совет может не сработать
Полный запрет дублей ломает легитимные сценарии, где параметры действительно представляют список. Например, фильтры каталога, поиск по тегам, массовые операции и некоторые старые формы. Поэтому правило «запретить все повторы» удобно для административных и финансовых действий, но грубо для поиска и отчетов.
Второй риск связан с совместимостью. Старые клиенты могут годами отправлять повторяющиеся параметры, потому что прежний сервер молча выбирал одно значение. Резкое включение отказа 400 может сломать мобильное приложение, интеграцию партнера или платежный сценарий. Перед ужесточением стоит включить режим наблюдения, собрать статистику дублей и только потом блокировать.
Третий риск лежит в логах. Если журналирование пишет только уже разобранные параметры, команда может не увидеть атаку. При расследовании будет казаться, что клиент отправил обычный запрос, хотя в сырой строке запроса были дубли, закодированные разделители или неожиданный порядок параметров.
FAQ
HPP работает только через GET-параметры?
Нет. Чаще всего HPP показывают на URL, но похожая проблема возникает в теле формы с типом application/x-www-form-urlencoded. Важна не только часть запроса, а способ разбора параметров.
Если приложение всегда берет первое значение, уязвимости нет?
Не обязательно. Если все слои системы действительно берут первое значение и такой контракт закреплен, риск ниже. Но на практике WAF, фреймворк, внутренний API и код приложения часто ведут себя по-разному.
Нужно ли блокировать все повторяющиеся параметры?
Для критичных одиночных параметров да. Для списков нужен явный формат и отдельная проверка. Например, tag может повторяться, а amount или role нет.
Можно ли закрыть HPP только настройкой WAF?
Нельзя считать WAF единственной защитой. WAF должен ловить аномалии, но приложение обязано само нормализовать параметры и отклонять недопустимые повторы.
Чем HPP отличается от обычной подмены параметра?
При обычной подмене меняют одно значение. При HPP отправляют несколько значений с одним именем и используют разницу в том, какое значение выберет каждый слой системы.
Короткий вывод для инженеров
Parameter Pollution опасен не трюком с двумя одинаковыми параметрами, а организационной дырой между слоями системы. Если разные компоненты по-разному читают один запрос, безопасность начинает зависеть от случайности: кто первым разобрал строку, кто взял последнее значение, кто склеил массив и кто записал в лог уже очищенную версию.
Надежная защита выглядит приземленно: один слой разбора параметров, явный список разрешенных массивов, отказ при повторе критичных полей, безопасная сборка внутренних URL, тесты на дубли и нормальные журналы. Такая работа не выглядит эффектно, зато убирает целый класс странных обходов, которые появляются на стыке WAF, фреймворка и внутреннего API.
Материал предназначен для легального анализа защищенности собственных систем, учебных стендов и проектов, где у вас есть явное разрешение на проверку. При тестировании реальных сервисов соблюдайте законы своей страны, включая законодательство России, правила площадок и договоры с владельцами систем. Не используйте описанные приемы для обхода ограничений, несанкционированного доступа, вмешательства в чужие сервисы или сокрытия действий.