CSRF, XSS, SQLi, uploads, CSP, security headers и секреты
Статус: основная часть актуальна для Django 5.2/6.0. Встроенная Content Security Policy появилась в Django 6.0; для 5.2 политику задают на reverse proxy или поддерживаемым сторонним пакетом.
Django предоставляет безопасные механизмы, но не может угадать архитектуру приложения. ORM не спасёт raw SQL с конкатенацией, autoescape — HTML, помеченный safe, а CSRF middleware — действие, которое ошибочно выполняется через GET.
Hardening начинается не со списка флагов, а с модели угроз: какие данные важны, кто имеет к ним доступ, где проходят trust boundaries и как обнаружить нарушение. Настройки ниже — обязательные слои, но не замена этой модели.
Устанавливайте последние security/bugfix-релизы выбранной поддерживаемой ветки Django и Python. Фиксируйте зависимости с проверяемыми lock-файлами, отслеживайте advisories и регулярно обновляйте не только Django, но и ASGI/WSGI server, драйвер базы, Pillow, parser и authentication-пакеты.
Перед deployment запускайте:
python manage.py check --deploy --settings=config.settings.productionSystem checks находят типовые ошибки настроек, но не знают, правильно ли proxy очищает заголовки, закрыт ли storage bucket и не допускает ли код horizontal privilege escalation. Результат проверки — начало ревью, а не сертификат безопасности.
import os
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = ["app.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SECURE = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = "same-origin"
SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin"
X_FRAME_OPTIONS = "DENY"ALLOWED_HOSTS защищает обработку Host header, если код использует request.get_host(). Не читайте необработанный HTTP_HOST и не используйте "*" без собственной строгой валидации.
CSRF_TRUSTED_ORIGINS — список origins, которым разрешено присылать небезопасные запросы с точки зрения CSRF. Он не настраивает CORS и не делает origin «доверенным» для чтения ответов браузером.
Если TLS завершается на доверенном proxy, Django может видеть внутренний HTTP. Только когда proxy удаляет входной заголовок клиента и сам выставляет корректное значение, настройте:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")Ошибочная доверенность к X-Forwarded-Proto влияет на request.is_secure(), HTTPS redirect и CSRF. Проверяйте всю цепочку CDN/load balancer/proxy, а не копируйте настройку вслепую.
HSTS сообщает браузеру, что домен нужно открывать только по HTTPS. Ошибка может сделать сайт недоступным на весь заданный срок.
Начните с небольшого значения и наблюдения:
SECURE_HSTS_SECONDS = 300После проверки всех маршрутов увеличивайте срок. SECURE_HSTS_INCLUDE_SUBDOMAINS = True безопасен только тогда, когда каждый поддомен поддерживает HTTPS. SECURE_HSTS_PRELOAD = True лишь добавляет директиву; включение домена в browser preload list — отдельная внешняя и трудно обратимая процедура.
Первое правило — GET, HEAD и другие safe methods не изменяют состояние. Тогда CsrfViewMiddleware защищает небезопасные запросы с cookie-аутентификацией.
<form method="post">
{% csrf_token %}
{{ form.as_div }}
<button type="submit">Сохранить</button>
</form>Для JavaScript-клиента токен передают в X-CSRFToken. Не ставьте @csrf_exempt на endpoint только потому, что он возвращает JSON: формат ответа не меняет модель атаки. Bearer credential из заголовка, который браузер не добавляет автоматически, обычно не создаёт ту же CSRF-модель; JWT в cookie снова требует защиты.
SameSite и проверка Origin/Referer дополняют CSRF token, но не оправдывают изменение данных через GET.
Django Template Language экранирует переменные в HTML text/attribute context:
<p>{{ comment.text }}</p>Это не универсальная санитизация. Опасные места:
|safe, mark_safe() и {% autoescape off %};Для передачи данных в script используйте json_script, а затем разбирайте JSON:
{{ chart_data|json_script:"chart-data" }}
<script nonce="{{ csp_nonce }}">
const chartData = JSON.parse(
document.getElementById("chart-data").textContent
);
</script>Если продукт действительно принимает пользовательский HTML, очищайте его серверной allowlist-библиотекой, поддерживайте эту библиотеку в актуальном состоянии и отдельно проверяйте ссылки/атрибуты. «Мы доверяем автору» не защищает от украденной учётной записи.
HttpOnly мешает JavaScript прочитать session cookie, но XSS всё ещё может отправлять запросы от имени пользователя. Поэтому устранение XSS и CSP важнее надежды на один cookie-флаг.
ORM параметризует значения:
User.objects.filter(username=user_input)Raw SQL тоже безопасен при placeholders для значений:
with connection.cursor() as cursor:
cursor.execute(
"SELECT id, username FROM accounts_user WHERE id = %s",
[user_id],
)Нельзя параметризовать имя столбца тем же способом. Если клиент выбирает сортировку, сопоставьте публичное значение с известным ORM-полем:
ORDERING = {
"newest": "-created_at",
"title": "title",
}
ordering = ORDERING.get(request.GET.get("sort"), "-created_at")
queryset = Post.objects.order_by(ordering)Не собирайте SQL через f-string, %, .format() или конкатенацию. Особого внимания требуют RawSQL, extra(), собственные database functions и параметры where/table сторонних библиотек.
Обращение os.environ["..."] заставляет приложение завершиться при отсутствии секрета. os.environ.get() без проверки может тихо передать None или небезопасный fallback.
Храните production-секреты в platform secret store, Vault/KMS или защищённых переменных окружения. .env удобен локально, но должен быть исключён из Git; .env.example содержит только имена и фиктивные значения.
Разделяйте ключи по окружениям и назначению. При ротации Django SECRET_KEY можно временно читать старые подписи через SECRET_KEY_FALLBACKS, оставляя новый ключ основным. Fallback увеличивает поверхность проверки, поэтому старый ключ удаляют после разумного периода.
Если секрет уже попал в Git, удаления из последнего commit недостаточно: считайте его скомпрометированным, отзовите/смените и проверьте историю и логи.
Не пишите в логи пароли, session IDs, CSRF tokens, Authorization, полные webhook payload с персональными данными и строки подключения. Настройте фильтрацию как в приложении, так и в proxy/APM.
Secure разрешает отправку cookie только по HTTPS. HttpOnly запрещает чтение из JavaScript. SameSite ограничивает часть cross-site отправок. У каждого флага своя задача; ни один не шифрует значение и не заменяет TLS.
CSRF_COOKIE_HTTPONLY=True не даёт значимого общего выигрыша от XSS: CSRF token присутствует в DOM формы, а XSS уже может выполнять действия. Кроме того, JavaScript-клиенту тогда приходится получать токен из DOM. Включайте настройку только с понятным клиентским flow.
После login Django меняет session key для защиты от session fixation. Для критичных операций полезны повторная аутентификация, ограничение возраста сессии и возможность завершить активные сессии пользователя.
Проверка расширения — только первый фильтр. Имя .jpg не доказывает, что содержимое является изображением, а MIME клиента недостоверен.
Практический pipeline:
DATA_UPLOAD_MAX_MEMORY_SIZE не ограничивает общий размер multipart-файлов так, как часто предполагают. Django прямо рекомендует ограничить body на production web server; под ASGI запрос может успеть попасть во временный файл до прикладной проверки.
Пользовательские файлы лучше отдавать с отдельного origin без application cookies и без возможности выполнения HTML/JS в контексте основного домена. Для скачивания неизвестных типов используйте Content-Disposition: attachment и X-Content-Type-Options: nosniff. Приватный storage не должен становиться публичным только из-за знания URL; выдавайте короткоживущую ссылку после permission-проверки.
Современная конфигурация storage:
STORAGES = {
"default": {
"BACKEND": "myproject.storage.PrivateMediaStorage",
},
"staticfiles": {
"BACKEND": (
"django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
),
},
}Удалено в Django 5.1: настройки
DEFAULT_FILE_STORAGEиSTATICFILES_STORAGE. Их заменила настройкаSTORAGES. Старые имена могут встретиться в legacy-проектах на Django 4.2/5.0.
Django 6.0 умеет формировать CSP без стороннего middleware:
from django.utils.csp import CSP
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.csp.ContentSecurityPolicyMiddleware",
# ...
]
SECURE_CSP_REPORT_ONLY = {
"default-src": [CSP.SELF],
"script-src": [CSP.SELF, CSP.NONCE],
"object-src": [CSP.NONE],
"base-uri": [CSP.SELF],
"frame-ancestors": [CSP.NONE],
"report-uri": "/csp-report/",
}Сначала соберите отчёты, устраните inline-скрипты и ложные нарушения, затем перенесите проверенную политику в SECURE_CSP. Report-only ничего не блокирует.
Для nonce добавьте django.template.context_processors.csp и используйте переменную csp_nonce только в тех шаблонах, где nonce действительно нужен. HTML с nonce и соответствующий CSP header должны быть сформированы одним запросом: готовую страницу нельзя независимо от заголовка раздавать из общего кэша. Не добавляйте 'unsafe-inline' ради быстрого исчезновения ошибок: это заметно ослабляет защиту script policy.
CSP уменьшает последствия многих XSS, но не исправляет небезопасный HTML и не заменяет escaping. frame-ancestors — современная защита от framing; X_FRAME_OPTIONS полезен как совместимый дополнительный слой.
Для Django 5.2 применяют заголовок на reverse proxy или актуальный django-csp. Его старые имена настроек не следует смешивать с SECURE_CSP Django 6.0.
Случайный URL admin уменьшает фоновый шум сканеров, но не является контролем доступа. Надёжные меры:
Сторонние пакеты вроде django-axes и MFA-решений оценивайте по совместимости с выбранной версией Django, proxy-схемой и политикой блокировки. Блокировка только по IP может устроить denial of service пользователям за общим NAT.
CORS управляет тем, какие браузерные origins могут читать ответ. Он не аутентифицирует запрос, не заменяет CSRF и не защищает server-to-server API. Не сочетайте отражаемый любой Origin с credentials.
Если пользователь задаёт URL для импорта, webhook-проверки или превью, появляется SSRF. Используйте allowlist назначения, запрещайте private/link-local/metadata ranges после DNS resolution, повторяйте проверку на redirect, ограничивайте протоколы, размер, время и число переходов. Лучше выполнять такие запросы из изолированной сети без доступа к внутренним сервисам.
DEBUG=False, точный ALLOWED_HOSTS, секреты вне Git;safe, raw SQL и динамических identifiers;SECURE_BROWSER_XSS_FILTER; SecurityMiddleware больше не устанавливает X-XSS-Protection. Используйте CSP без небезопасного inline-кода.DEFAULT_FILE_STORAGE и STATICFILES_STORAGE; используйте STORAGES./admin/ на секретный путь не считается защитой от brute force.CSRF_TRUSTED_ORIGINS не является настройкой CORS и не должно содержать домены «на всякий случай».Далее: Асинхронный Django