SSRF: Обход проверки URL через HTTP-редирект в Node.js
Админ-форма импорта продуктового фида проверяет hostname URL и отвергает loopback и приватные сети, но fetch() автоматически следует HTTP-редиректам — внешний сервер атакующего перенаправляет на внутренний /admin/stats и утечка флага.
hardnodejsPro
Задача
# SSRF: обход проверки URL через HTTP-редирект
## Сценарий
Перед вами Express-приложение SecureShop — интернет-магазин с админ-панелью. В админке есть удобная фича «Import Product Feed»: оператор подаёт URL партнёрского фида, сервис делает к нему пробный `GET`-запрос и показывает тело ответа, чтобы убедиться, что партнёр действительно отвечает `200 OK` и возвращает ожидаемый формат, прежде чем сохранить адрес в production-конфигурации.
Команда понимала, что такой инструмент потенциально опасен: если оставить его без проверок, любой авторизованный администратор сможет «подсмотреть» внутренние сервисы, до которых снаружи доступа нет. Поэтому в код встроена защита: перед каждым запросом hostname URL сверяется со списком запрещённых сетей — loopback (`127.0.0.0/8`, `::1`), приватные RFC1918 (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), link-local (`169.254.0.0/16`). Если URL ведёт в одну из этих сетей — сервис отвечает `403 Forbidden` и до сетевого вызова дело не доходит.
Дополнительно фильтр содержит «удобство для разработки»: список доверенных адресов через переменную окружения `PUBLIC_HOST_OVERRIDES` (типичный enterprise-паттерн — staging-прокси на приватной подсети, который маршрутизируется в публичную сеть через SNAT). Хосты из этого списка пропускаются без проверки сети.
В том же приложении живёт внутренний админ-эндпоинт `/admin/stats`, который возвращает JSON со статистикой и CTF-флагом. В production его слушает только loopback-интерфейс. Прямая попытка передать форме импорта фида loopback-адрес отвергается на старте: проверка hostname видит запрещённую сеть и блокирует запрос.
Но защита проверяет **только начальный URL**. Поведение HTTP-клиента после первого ответа сервиса остаётся незащищённым — а именно там скрывается уязвимость класса **SSRF (Server-Side Request Forgery)**. Ваша задача — построить такой сценарий, в котором проверка hostname проходит на старте, но финальный запрос всё равно уходит на внутренний админ-эндпоинт, и в ответе оказывается CTF-флаг.
## Цели
1. Войдите как администратор и изучите форму «Import Product Feed»; эмпирически убедитесь, что прямой адрес внутреннего эндпоинта блокируется проверкой hostname.
2. Постройте цепочку, в которой начальный URL для проверки выглядит «внешним», а итоговый сетевой запрос всё равно попадает на `/admin/stats`.
3. Извлеките CTF-флаг из JSON-ответа внутреннего эндпоинта.
## Данные
| Параметр | Значение |
|----------|----------|
| Форма импорта фида | `POST /admin/import-feed` (параметры: `url`, `csrf_token`) |
| Эндпоинт админ-панели | `GET /admin` |
| Эндпоинт с флагом | `GET /admin/stats` (отдаёт JSON с `flag`) |
| Учётные данные администратора | `admin / admin` |
| Допустимые схемы URL | `http://`, `https://` |
| Запрещённые сети начального URL | loopback, RFC1918, link-local |
> Подсказка: HTTP-протокол поддерживает не только прямые ответы. Если внешний сервер отвечает `30x` с заголовком `Location:`, многие HTTP-клиенты молча выполняют второй сетевой запрос — уже без всякой повторной валидации. Подумайте, что произойдёт, если адресом «второго запроса» окажется loopback-адрес внутреннего админ-эндпоинта.