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 УК РФ).
Проверьте себя
1. Почему SSRF особенно опасен, хотя запрос делает «всего лишь сервер»?
AСерверу доступны внутренние ресурсы (localhost, приватные подсети, служебные эндпоинты), которые недоступны снаружи и часто защищены слабее
BСервер всегда выполняет запросы быстрее клиента
CЗапрос сервера невозможно залогировать
DБраузер жертвы исполняет ответ как скрипт
2. Какой набор мер правильно закрывает небезопасную загрузку файлов?
AДоверять MIME-типу из запроса и сохранять файл под присланным именем
BAllowlist расширений + собственное сгенерированное имя файла + хранение вне исполняемого каталога + лимит размера
CЗапретить загрузку только файлов с расширением .exe
DСкладывать загрузки в ту же папку, что и код, но переименовывать их