Безопасность Django: где встроенная защита не спасёт
Django закрывает CSRF, XSS и SQL-инъекции из коробки, но каждую защиту легко отключить одной строкой. Разбираем, где механизмы ломаются и почему SSRF Django не ловит вообще.

Django из коробки закрывает три из четырёх классов уязвимостей OWASP, о которых спорят на код-ревью: CSRF-токен в каждой форме, экранирование в шаблонах, параметризация в ORM. Проблема в том, что каждую из этих защит разработчик отключает одной строкой — иногда осознанно, чаще по невнимательности. А SSRF Django не ловит вообще: это не его зона ответственности, и разработчики про неё забывают. Разберём на уровне механизма: как именно работает каждая защита, при каком коде она перестаёт работать и что писать в комментарии к пул-реквесту. Актуально для Django 4.2 LTS и 5.x.
CSRF: токен есть, но проверка отключается тремя способами
Механизм простой. Middleware django.middleware.csrf.CsrfViewMiddleware на каждый небезопасный метод (POST, PUT, PATCH, DELETE) сверяет токен из тела запроса или заголовка с токеном из cookie. Токен привязан к сессии через секрет и маскируется случайной солью, поэтому его нельзя переиспользовать между пользователями. Пока вы пишете формы через шаблонный тег {% csrf_token %}, всё работает само.
Защита перестаёт работать в трёх ситуациях, и все три встречаются на ревью регулярно:
- Декоратор
@csrf_exemptна вьюхе. Часто вешают на webhook-эндпоинт «чтобы платёжка достучалась», а потом забывают, что этот же URL теперь принимает POST без токена от кого угодно. Если за эндпоинтом есть действие с побочным эффектом — это дыра. - API на DRF с SessionAuthentication. Django REST Framework применяет CSRF-проверку только к сессионной аутентификации. Если у вас
SessionAuthenticationв списке классов, а фронтенд ходит с cookie, но без заголовкаX-CSRFToken— запросы будут падать или, хуже, вы отключите проверку целиком, чтобы «заработало». - Правка
CSRF_TRUSTED_ORIGINSвслепую. В Django 4.x формат этой настройки требует схему (https://example.com, а не простоexample.com). Люди добавляют туда лишние домены при отладке CORS и оставляют в проде.
На ревью формулируйте замечание конкретно, а не «проверьте CSRF»:
«Здесь @csrf_exempt на вьюхе, меняющей данные пользователя. Если это webhook — нужна отдельная проверка подписи из заголовка, а не отключение CSRF. Если это внутренний вызов — уберите декоратор и передавайте токен.»
XSS: автоэкранирование включено, пока вы его не выключили
Шаблонизатор Django по умолчанию экранирует <, >, &, кавычки во всех переменных. То есть {{ user_input }} безопасно, даже если внутри лежит <script>. Экранирование выключается тремя способами, и каждый — потенциальная точка входа.
Фильтр |safe и тег {% autoescape off %}
Оба говорят шаблонизатору «доверяю этой строке, не трогай её». Опасны, когда в помеченную |safe строку попадают пользовательские данные. Классический сценарий: поле «описание» с HTML-разметкой выводят через {{ description|safe }}, а пользователь вписывает туда <img src=x onerror=alert(document.cookie)>.
Правильный путь для пользовательского HTML — санитайзить его на входе или выводе через отдельную библиотеку (например, nh3 — привязка к Rust-библиотеке ammonia, активно поддерживается; bleach объявлен deprecated, новые проекты на него лучше не сажать). Санитайзер оставляет разрешённый набор тегов и вырезает опасные атрибуты:
# pip install nh3
import nh3
# Разрешаем только форматирование, вырезаем script, on*-атрибуты и javascript:
safe_html = nh3.clean(
user_description,
tags={"p", "b", "i", "a", "ul", "ol", "li", "br"},
attributes={"a": {"href", "title"}},
)
# Теперь safe_html можно отдавать в шаблон через |safe
Смысл: |safe ставится только на строку, которую вы сами очистили, а не на сырой ввод.
Атрибуты и JavaScript-контекст
Автоэкранирование заточено под HTML-контекст. Оно не спасёт, если вы вставляете переменную внутрь <script> или в обработчик события. Строка вида <a onclick="go('{{ value }}')"> уязвима, даже с включённым экранированием, потому что кавычки в JS-строке экранируются не так, как в HTML. Для передачи данных в JS используйте тег {{ data|json_script:"my-data" }} — он кладёт данные в <script type="application/json"> с корректным экранированием, а из JS вы их читаете через JSON.parse.
SQL-инъекции: ORM параметризует всё, кроме того, что вы передали руками
ORM Django параметризует запросы: User.objects.filter(name=value) уходит в базу как WHERE name = %s с отдельно переданным значением. SQL-инъекция через штатный API невозможна. Ломается это там, где разработчик обходит ORM.
.extra()и.raw()с f-строкой. КонструкцияMyModel.objects.raw(f"SELECT * FROM app_model WHERE name = '{name}'")— прямая инъекция. Правильно передавать параметры вторым аргументом:.raw("SELECT * FROM app_model WHERE name = %s", [name]).cursor.executeсо склейкой строк. То же правило:cursor.execute("... WHERE id = %s", [obj_id]), никакой конкатенации.RawSQLиextra(select=...). Если внутрь попадает имя столбца или таблицы из пользовательского ввода — параметризация тут не помогает (плейсхолдеры работают для значений, не для идентификаторов). Имена столбцов проверяйте по белому списку.
Отдельная ловушка — сортировка по полю из GET-параметра: queryset.order_by(request.GET['sort']). Инъекции классического вида тут нет, но пользователь может отсортировать по любому полю модели, включая связанные, и вытащить данные через related-обращения. Разрешённые поля держите в явном списке.
SSRF: Django не защищает, потому что это не его слой
Server-Side Request Forgery возникает, когда ваш сервер сам делает HTTP-запрос по URL, который контролирует пользователь. Классика: «загрузите аватар по ссылке», превью для URL, вебхуки, импорт по адресу. Атакующий подставляет http://169.254.169.254/latest/meta-data/ (метаданные облака с временными ключами) или http://localhost:6379/ (внутренний Redis), и ваш сервер послушно ходит туда изнутри периметра.
Django здесь ничего не проверяет — запрос делаете вы через requests, httpx или urllib, и валидация на вашей совести. Наивная проверка «резолвим домен, смотрим, не приватный ли IP» ломается двумя способами: DNS rebinding (домен резолвится в публичный IP на проверке и в 127.0.0.1 на самом запросе) и редиректы (внешний URL отвечает 302 на внутренний адрес).
Минимальный набор мер против SSRF:
- Белый список схем и портов. Только
http/https, только 80/443. Отсекаетfile://,gopher://,ftp://. - Резолвьте адрес и проверяйте его до запроса модулем
ipaddress: блокируйтеis_private,is_loopback,is_link_local(сюда попадает169.254.0.0/16— метаданные облаков),is_reserved. - Запретите редиректы в HTTP-клиенте (
allow_redirects=Falseвrequests) или проверяйте каждый хоп заново. - Сетевой уровень. Самая надёжная защита — не пускать исходящий трафик приложения к внутренним подсетям и к
169.254.169.254на уровне firewall или egress-политики. Код можно обойти, сетевое правило — сложнее.
# Django 5.x, requests 2.x — проверка адреса перед запросом
import ipaddress
import socket
from urllib.parse import urlparse
ALLOWED_SCHEMES = {"http", "https"}
def is_url_safe(url: str) -> bool:
parsed = urlparse(url)
if parsed.scheme not in ALLOWED_SCHEMES:
return False
host = parsed.hostname
if not host:
return False
try:
# Резолвим все адреса, а не только первый
infos = socket.getaddrinfo(host, None)
except socket.gaierror:
return False
for info in infos:
ip = ipaddress.ip_address(info[4][0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
return False
return True
# Внимание: между этой проверкой и самим запросом есть окно DNS rebinding.
# Для строгой защиты резолвьте адрес один раз и ходите по IP, а не по имени,
# либо закрывайте внутренние подсети на уровне egress firewall.
Комментарий в коде не для красоты: проверка выше — необходимый минимум, но не полная гарантия. Окно между DNS-резолвом при валидации и резолвом при реальном запросе остаётся, и закрывается оно надёжно только на сетевом уровне.
Что настроить, помимо кода
Часть защиты живёт в настройках, а не во вьюхах. Проверьте перед выкладкой в прод:
| Настройка | Значение в проде | Что закрывает |
|---|---|---|
DEBUG | False | Утечку traceback, SQL-запросов и переменных окружения на странице ошибки |
ALLOWED_HOSTS | Явный список доменов | Host header injection, отравление кеша |
SECURE_SSL_REDIRECT | True | Работу по HTTP, где токены и cookie летят открытым текстом |
SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURE | True | Утечку cookie по незашифрованному соединению |
SECURE_HSTS_SECONDS | Ненулевое (напр. 31536000) | Первичный перехват до редиректа на HTTPS |
Django умеет проверить конфигурацию сам. Команда python manage.py check --deploy выводит предупреждения по настройкам безопасности — прогоняйте её в CI перед деплоем, а не читайте чек-листы по памяти.
Что делать дальше
Первый шаг — прогнать manage.py check --deploy на текущем проекте и закрыть каждое предупреждение осознанно, а не подавить их скопом. Дальше — grep по кодовой базе: csrf_exempt, |safe, autoescape off, .raw(, .extra(, mark_safe. Каждое вхождение должно иметь объяснение, почему здесь защита отключена безопасно. Для исходящих запросов найдите все места, где URL приходит от пользователя, и убедитесь, что перед запросом есть валидация и запрет редиректов. На проде смотрите логи исходящих соединений приложения: обращения к 169.254.169.254 или к внутренним подсетям от веб-процесса — сигнал, что кто-то щупает SSRF.
Инфографика для маркетплейса: Три главных преимущества товара короткими фразами, выноски в безопасной зоне превью, точный размер площадки.
Спросить за 60 ₽Источники: Security in Django — официальная документация, OWASP Top Ten
Частые вопросы
У меня Django 3.2, а не 4.2 — примеры сработают?
Достаточно ли автоэкранирования шаблонов, чтобы не думать про XSS?
Я поставил @csrf_exempt на webhook и всё заработало — это нормально?
Если проверка SSRF пропустила запрос, а он всё равно ушёл на внутренний адрес — почему?
ORM полностью защищает от SQL-инъекций?
Стоит ли использовать bleach для санитайзинга HTML?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.