SSRF и небезопасная загрузка файлов
Две серверные уязвимости рядом: как сервер обманом заставляют сходить «не туда» и как он принимает опасный файл — и как закрыть обе на стенде.
SSRF (Server-Side Request Forgery) — уязвимость, при которой приложение делает сетевой запрос по адресу, который контролирует пользователь, и тем самым обращается к ресурсам от лица сервера.
Обе темы изучаем на учебных стендах (DVWA, Juice Shop, локальная ВМ): они показывают, как доверие к пользовательскому вводу на стороне сервера приводит к проблемам. Заставлять чужой сервер ходить во внутреннюю сеть или загружать на него опасные файлы без разрешения — это статьи 272/273 УК РФ. Только своя лаборатория.
Зачем это знать защитнику
SSRF и небезопасная загрузка особенно опасны в облаке и микросервисах: «внутренние» адреса, доступные только серверу, обычно защищены слабее, чем внешний периметр. Разработчик, понимающий эти атаки, перестаёт доверять URL и файлам, пришедшим от пользователя, и ставит правильные ограничения.
SSRF: как сервер обманывают на запрос
Уязвимость рождается, когда приложение берёт URL из ввода и идёт по нему (загрузка картинки по ссылке, превью, импорт по URL, вебхук):
# УЯЗВИМО: сервер запрашивает любой URL пользователя
@app.post("/api/fetch-image")
def fetch_image():
url = request.json["url"]
resp = requests.get(url) # адрес полностью контролирует клиент
return resp.content
Ожидается внешняя картинка. Но запрос делает сервер — а ему доступно то, что недоступно вам снаружи: внутренние сервисы, адреса вида 127.0.0.1 и приватные диапазоны, в облаке — служебные эндпоинты метаданных. Подменив URL на внутренний адрес, пользователь заставляет сервер обратиться к ресурсу за периметром. Это и есть «forgery»: запрос как бы исходит от доверенного сервера. Мы разбираем принцип, а не способ добраться до конкретной инфраструктуры.
Как это находят на CTF
На стенде в поле URL подставляют адрес, указывающий обратно на сервер или в его внутреннюю сеть, и смотрят: изменился ли ответ, появилась ли задержка, видно ли содержимое, которого снаружи быть не должно. Для подтверждения часто используют свой контролируемый слушатель и проверяют, пришёл ли к нему запрос от сервера. Цель — найти эндпоинт, который ходит по непроверенному URL, чтобы закрыть его.
Небезопасная загрузка файлов
Вторая половина урока — загрузка. Уязвимый обработчик доверяет тому, что прислал клиент: имени файла и заявленному типу:
# УЯЗВИМО: доверяем имени и кладём в общедоступную папку
f = request.files["upload"]
f.save("/var/www/uploads/" + f.filename) # имя и расширение от клиента
Здесь сразу несколько проблем. Расширение и MIME-тип присылает клиент — им нельзя верить. Имя файла может содержать ../ и увести запись за пределы папки (path traversal). Если загруженный файл попадает в каталог, который сервер исполняет как код, принятый скрипт может быть запущен запросом к нему. Корень — доверие к метаданным клиента и хранение загрузок там, где их исполняют.
Как это работает под капотом
В обоих случаях сервер принимает решение на основе данных, которым не должен доверять. При SSRF доверенным считается произвольный URL, и сетевой стек честно выполняет соединение — у него нет понятия «этот адрес внутренний и трогать нельзя». При загрузке доверенными считаются имя и тип из тела запроса, хотя их формирует клиент. Защита в обоих случаях строится на одном принципе: не доверяй вводу, разрешай явно, проверяй сам, изолируй последствия.
Как защититься
SSRF — 1. Allowlist назначений. Не «запрещаем плохие адреса» (чёрный список обходится редиректами и DNS-трюками), а разрешаем только нужные домены/хосты. Проверяйте, что итоговый адрес ведёт во внешний разрешённый ресурс, и запрещайте приватные диапазоны:
# БЕЗОПАСНО: только явный allowlist хостов
from urllib.parse import urlparse
ALLOWED = {"images.example.com", "cdn.example.com"}
host = urlparse(url).hostname
if host not in ALLOWED:
abort(400) # всё, что не в списке — отклоняем
resp = requests.get(url, timeout=5, allow_redirects=False)
SSRF — 2. Сетевая изоляция и запрет редиректов. Отключайте автоматические редиректы (иначе разрешённый адрес может увести на внутренний). На уровне сети закрывайте серверу доступ к внутренним подсетям и служебным эндпоинтам метаданных — тогда даже при ошибке в коде идти «внутрь» некуда. Это защита в глубину: код + сеть.
Загрузка — 1. Валидируйте сами, по содержимому. Не верьте присланному типу — проверяйте файл на сервере и сверяйте с allowlist расширений:
# БЕЗОПАСНО: allowlist расширений + своё безопасное имя
import os, uuid
ALLOWED_EXT = {".png", ".jpg", ".jpeg", ".webp"}
ext = os.path.splitext(f.filename)[1].lower()
if ext not in ALLOWED_EXT:
abort(400)
safe_name = uuid.uuid4().hex + ext # генерируем имя сами
f.save(os.path.join(UPLOAD_DIR, safe_name))
Своё случайное имя убирает path traversal (никаких ../ от клиента) и перезапись чужих файлов.
Загрузка — 2. Изолируйте хранилище. Держите загрузки вне каталога, который сервер исполняет: в отдельном хранилище/бакете, отдавайте их как статику или через прокси, без права выполнения. Тогда даже принятый скрипт не запустится. Ограничивайте размер файла, чтобы избежать переполнения диска.
Обнаружение. Логируйте исходящие запросы сервера к необычным адресам (особенно во внутренние диапазоны) и загрузки с отклонёнными расширениями — это сигналы попытки эксплуатации SSRF и загрузки.
Итоги
- SSRF заставляет сервер сделать запрос по контролируемому пользователем URL и достучаться до внутренних ресурсов от его имени.
- Небезопасная загрузка возникает из доверия к имени/типу файла от клиента и хранения загрузок в исполняемом каталоге.
- Общий корень — доверие к вводу на стороне сервера; общий принцип защиты — allowlist, своя валидация, изоляция.
- SSRF: разрешённый список хостов, запрет приватных диапазонов и редиректов, сетевая изоляция сервера.
- Загрузка: allowlist расширений, своё имя файла, хранение вне исполняемого каталога, лимит размера. Практика — только на стенде (ст. 272/273 УК РФ).