Endpoint принимает URL изображения и скачивает его сервером. Цена ошибки — превратить обычное поле формы в доступ к внутреннему адресу, metadata-сервису или административному интерфейсу. Проверка только на строку https:// не защищает: URL имеет имя, порт, учетные данные, редирект и адрес, которые меняют фактическую цель.
Разберём защиту до вызова сети: разрешённая схема, точное имя хоста, запрет credentials и явное ограничение портов. Учебная функция не делает запрос. Она принимает строку и возвращает нормализованный адрес либо причину отказа, поэтому её можно запускать локально и проверять отдельными примерами.
Проверяем не строку, а разобранный адрес
Сначала парсер URL должен превратить строку в структуру. Затем политика проверяет протокол, hostname, порт и наличие логина или пароля. Проверять только исходное начало строки недостаточно: https://trusted.example@127.0.0.1/ имеет доверенное имя до символа @, но фактический host — loopback.
| Поле | Допустимое правило | Причина |
|---|---|---|
| protocol | только https: | не отправлять секрет по plain HTTP |
| hostname | точный allowlist | не доверять суффиксу строки |
| port | 443 или явно разрешённый | сократить обход сервисов |
| username/password | пусто | не передавать credentials дальше |
| pathname | проверяется отдельно | не разрешать опасный endpoint |
Allowlist должна быть точной
Сравнение hostname.endsWith("example.com") пропустит evil-example.com. Сравнение с одним каноническим именем проще проверить. Если нужны поддомены, добавьте правило границы: имя равно базовому или заканчивается на .example.com. IP-адреса, IPv6 и DNS-редиректы требуют отдельного решения, потому что имя может измениться между проверкой и соединением.
Защита от SSRF — не только функция валидации. Клиент должен ограничить редиректы, timeout, размер ответа и набор портов. Сетевой слой может дополнительно запретить приватные диапазоны и loopback. Эти меры защищают разные границы, поэтому одна allowlist не заменяет остальные.
Учебный валидатор URL
Входом функции является строка URL и массив разрешённых имён. Ожидаемый результат — объект { allowed, reason, href }. Ниже нет fetch: пример проверяет предмет статьи и не выполняет опасное подключение. Для безопасных тестов используются только публичные доменные имена из allowlist.
function validateRemoteUrl(value, allowedHosts) {
let url;
try { url = new URL(value); } catch { return { allowed: false, reason: 'invalid-url' }; }
if (url.protocol !== 'https:') return { allowed: false, reason: 'scheme' };
if (url.username || url.password) return { allowed: false, reason: 'credentials' };
if (url.port && url.port !== '443') return { allowed: false, reason: 'port' };
if (!allowedHosts.includes(url.hostname)) return { allowed: false, reason: 'host' };
return { allowed: true, reason: 'allowlist', href: url.href };
}
console.log(validateRemoteUrl('https://cdn.example.test/file.jpg', ['cdn.example.test']));
console.log(validateRemoteUrl('https://cdn.example.test@127.0.0.1/file.jpg', ['cdn.example.test']));
console.log(validateRemoteUrl('https://127.0.0.1/file.jpg', ['cdn.example.test']));
// allowed true; allowed false, reason credentials; allowed false, reason hostПервая строка проходит, вторая останавливается на credentials, а третья — на фактическом hostname loopback. Порядок проверок важен: сначала исключаем встроенные учетные данные, затем сверяем host. В полном сервисе URL нельзя передавать в сеть сразу после этого результата: нужны лимит ответа, запрет небезопасных redirect и проверка адреса после разрешения имени там, где это входит в модель угроз.
Редирект — новый URL
Если разрешённый сервер отвечает 302 на другой адрес, политика должна решить, разрешены ли перенаправления. Автоматически следовать за ними опасно: проверка исходного host уже не описывает конечный. Безопасный вариант — отключить redirect и вернуть его адрес как проверяемую ошибку. Если redirect нужен, каждый новый URL проходит ту же проверку.
Также проверяйте размер ответа до чтения всего тела. Даже разрешённый host может вернуть гигабайты и занять память. Timeout ограничивает время, но не объём. Эти ограничения не делают endpoint безопасным сами по себе, зато уменьшают blast radius ошибки политики.
Порядок внедрения
- Назвать допустимые домены, схемы и порты в конфигурации, а не принимать их из запроса.
- Распарсить URL стандартным URL-парсером и проверить hostname после нормализации.
- Запретить credentials, неожиданные схемы и redirect по умолчанию.
- Ограничить timeout, размер ответа и число сетевых попыток.
- Добавить тесты на @, похожий домен, IP, IPv6, другой порт и redirect.
- Сохранить причину отказа без полного URL с секретными параметрами.
Ограничения и следующий шаг
Учебный валидатор не знает DNS rebinding, proxy и правила сетевого сегмента. Allowlist домена не доказывает, что адрес после разрешения безопасен. Для чувствительных систем нужен совместный контроль приложения и egress-сети, а также отдельные тесты redirect и размера ответа.
Следующим шагом возьмите один endpoint, который принимает URL, и выпишите допустимые host, port, redirect и максимальный размер. Затем добавьте отрицательные тесты, включая имя с @. Критерий готовности — запрещённый адрес не доходит до клиента сети.
Проверяемые источники
- NIST SP 800-218 — Secure Software Development Framework (SSDF) Version 1.1 — February 2022. Даёт общий словарь практик безопасной разработки и связывает защиту с жизненным циклом продукта. Граница применимости: Рамка не подтверждает наличие контроля, уязвимости или результата в конкретном приложении.
- OWASP Application Security Verification Standard — Version 5.0.0, released 30 May 2025. Даёт версионируемые требования для проверки технических контролей веб-приложения. Граница применимости: Стандарт не заменяет модель угроз, настройку инфраструктуры и тестирование конкретных данных.
- NIST SP 800-53 Rev. 5 — Security and Privacy Controls — September 2020, updates as of 10 December 2020. Помогает различить сам контроль и уверенность в его работе. Граница применимости: Каталог не выбирает права вашего ресурса и не является отчётом о соответствии.