Веб
Host header injection: простая на вид, но серьёзная уязвимость
Если приложение доверяет заголовку Host или X-Forwarded-Host, ссылки сброса пароля могут вести на чужой домен. Реальный случай, способ проверки и исправление в Laravel и nginx.
· 7 мин. чтения

Host header injection — уязвимость, которую многие разработчики не замечают. В коде нет бросающейся в глаза ошибки вроде SQL-инъекции или XSS, всё работает как обычно. Но если приложение слепо доверяет заголовку Host или X-Forwarded-Host из запроса, атакующий может направить ссылки, которые генерирует приложение, на свой домен.
Эта уязвимость была найдена в экспертизе нашего собственного продукта — SaaS-платформы для налогов и финансов. Ниже покажем, как она работает, как её проверить и как мы её закрыли.
Откуда берётся проблема
Приложения часто строят полные (абсолютные) URL: ссылки в письмах, редиректы, теги canonical, адреса файлов. Для этого они берут имя домена из текущего запроса. Фреймворк читает домен из заголовка Host, а если приложение стоит за прокси — из X-Forwarded-Host.
Проблема в том, что эти заголовки отправляет сам клиент. Если приложение доверяет X-Forwarded-Host по принципу «наверное, его добавил прокси», любой может указать любой домен.
Реальный случай: ссылка сброса пароля
В нашем случае приложение доверяло всем заголовкам X-Forwarded-*. Сценарий атаки выглядит так:
- Атакующий вводит email жертвы в форму восстановления пароля.
- Добавляет к запросу заголовок
X-Forwarded-Host: attacker.example. - Приложение строит ссылку сброса с этим доменом:
https://attacker.example/reset-password/<token>. - Жертва получает настоящее письмо с настоящего сайта. Если она нажмёт на ссылку, секретный токен уйдёт на сервер атакующего.
- Атакующий с этим токеном меняет пароль на настоящем сайте.
Письмо приходит с официального адреса, текст правильный, отличается только домен в ссылке. Поэтому уязвимость считается серьёзной.
Возможны и другие последствия: ссылки на чужой домен в кешируемых страницах (cache poisoning), редиректы на посторонний сайт.
Почему эта уязвимость долго остаётся незамеченной? Потому что при обычном использовании Host всегда приходит правильный, и в тестах всё работает. Проблема видна, только когда кто-то намеренно меняет заголовок. А в приложениях за прокси разработчики часто ставят настройку «доверять всему», чтобы сайт заработал, и потом о ней забывают.
Любой заголовок, который присылает клиент, — это пользовательский ввод.
Host— не исключение.
Как проверить
Самая простая проверка — отправить запрос с чужим доменом и посмотреть, появится ли он в ответе:
curl -si https://your-domain.uz/login \
-H "X-Forwarded-Host: attacker.example" | grep -i attacker
curl -si https://your-domain.uz/login \
-H "Host: attacker.example" | grep -i attacker
Также:
- Отправьте форму восстановления пароля с
X-Forwarded-Hostи проверьте ссылку в пришедшем письме (на своём тестовом аккаунте). - Посмотрите ответы-редиректы с заголовком
Location. - Попробуйте альтернативные заголовки:
X-Forwarded-Server,X-Host,Forwarded.
Если attacker.example появился в ответе или письме — уязвимость есть.
Исправление в Laravel
1. Ограничьте доверие к прокси
Middleware TrustProxies в Laravel определяет, каким прокси и каким заголовкам доверять. У нас проблема была именно здесь: доверие ко всем заголовкам. Исправление — доверять только X-Forwarded-For, Proto и Port, а X-Forwarded-Host игнорировать.
В Laravel 11+ в bootstrap/app.php:
use Illuminate\Http\Request;
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(
at: ['127.0.0.1'],
headers: Request::HEADER_X_FORWARDED_FOR
| Request::HEADER_X_FORWARDED_PORT
| Request::HEADER_X_FORWARDED_PROTO
);
})
В at указывайте только реальный адрес прокси, а не '*'.
2. Зафиксируйте root URL в production
Чтобы ссылки строились из конфигурации, а не из запроса, добавьте в AppServiceProvider::boot():
use Illuminate\Support\Facades\URL;
if ($this->app->isProduction()) {
URL::forceRootUrl(config('app.url'));
URL::forceScheme('https');
}
APP_URL в .env обязательно должен содержать правильный домен.
3. Разрешённые хосты
Как дополнительный слой включите middleware TrustHosts — он принимает запросы только с разрешённых доменов:
$middleware->trustHosts(at: ['your-domain.uz']);
Исправление в nginx
Помимо защиты на уровне приложения, веб-сервер тоже не должен пропускать к приложению запросы с чужим Host.
default_server
nginx направляет запрос в подходящий блок server по server_name. Если подходящего блока нет, запрос попадает в default_server, а если он не задан — в первый блок, то есть часто в ваш основной сайт. Поэтому добавьте отдельный «улавливающий» блок:
server {
listen 80 default_server;
listen 443 ssl default_server;
server_name _;
ssl_reject_handshake on;
return 444;
}
server {
listen 443 ssl;
server_name your-domain.uz;
# ... основной сайт
}
return 444 закрывает соединение без ответа, а ssl_reject_handshake (nginx 1.19.4+) отклоняет TLS-соединение для неизвестного домена.
Заголовки, передаваемые в прокси
Если nginx проксирует запросы в приложение, не пропускайте присланный клиентом X-Forwarded-Host и передавайте Host со значением сервера:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host "";
После исправления
Повторите команды curl выше: запрос с чужим Host должен отклоняться nginx, а X-Forwarded-Host — никак не влиять на ссылки. Проверьте ещё раз и письмо сброса пароля.
Итог
Host header injection не видна в коде — она живёт в настройках доверия. Исправление короткое: ограничить доверие к прокси, зафиксировать root URL и добавить default_server в nginx. Но об этом нужно знать и проверять заранее.
Хотите проверить, что ваш сайт не доверяет чужим заголовкам? Оставьте заявку на сканирование — Claude AI агент и наш специалист посмотрят вашу конфигурацию и помогут закрыть найденные дыры.


