Safeguard
Security Guides

Django Security Best Practices for 2026

Django ships with strong defaults, but misconfigured settings, raw ORM queries, and unpinned dependencies still cause real breaches. Here is the checklist that matters.

Priya Mehta
Security Researcher
6 min read

Most Django breaches are not framework failures. They are the result of a DEBUG = True that shipped to production, a raw SQL query built with an f-string, or a dependency that went three years without an upgrade. Django gives you a genuinely secure foundation, but the foundation only holds if you understand which protections are automatic and which ones you have to switch on yourself.

The short answer

Django protects you by default against SQL injection (through the ORM), XSS (through template auto-escaping), CSRF (through the CSRF middleware), and clickjacking (through X-Frame-Options). Your job is to avoid opting out of those protections, lock down deployment settings, and keep the dependency tree patched. Everything below is a concrete way to do that.

Lock down your settings module

The single highest-value change is treating settings.py as a security-critical file. These are the settings that turn into incidents when they drift:

import os

DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"

# Never hard-code this. A leaked SECRET_KEY lets an attacker forge
# session cookies and signed tokens.
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

# An empty or wildcard ALLOWED_HOSTS enables Host header poisoning.
ALLOWED_HOSTS = os.environ["DJANGO_ALLOWED_HOSTS"].split(",")

# HTTPS, HSTS, and secure cookies
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SECURE_CONTENT_TYPE_NOSNIFF = True

When DEBUG is true, Django serves detailed stack traces that leak source paths, settings, and local variables. It is the most common way a Django app hands an attacker a map of its internals.

Do not eyeball this list by hand. Django ships a deployment linter:

python manage.py check --deploy

Run it in CI on every pull request and treat warnings as failures.

Do not defeat the ORM

The Django ORM parameterizes queries for you. You lose that protection the moment you drop to raw SQL and build the string yourself:

# DANGEROUS - user input concatenated into SQL
User.objects.raw("SELECT * FROM users WHERE name = '%s'" % name)

# SAFE - parameters passed separately, never interpolated
User.objects.raw("SELECT * FROM users WHERE name = %s", [name])

# SAFE - the ORM parameterizes automatically
User.objects.filter(name=name)

The same rule applies to .extra() and QuerySet.annotate() with RawSQL. If you must use them, keep user data in the params argument. We go deeper on this in our guide to preventing SQL injection in Python, which covers the ORM escape hatches that quietly reintroduce risk.

Templates and the mark_safe trap

Django templates auto-escape variables, so {{ user_bio }} is safe even if the bio contains a script tag. The escape hatch is mark_safe() and the |safe filter, which tell Django "trust this string, render it raw." Every call to mark_safe() on data that originated from a user is a potential stored XSS.

from django.utils.html import format_html

# DANGEROUS - raw user content rendered without escaping
return mark_safe(f"<span>{user_input}</span>")

# SAFE - format_html escapes the arguments
return format_html("<span>{}</span>", user_input)

If you accept rich text, sanitize it server-side with a strict allowlist library such as nh3 or bleach before storing it, and never trust client-side sanitization alone.

CSRF, sessions, and authentication

Keep django.middleware.csrf.CsrfViewMiddleware enabled and use {% csrf_token %} in every form. Do not sprinkle @csrf_exempt on views to make an integration work; carve out a token-authenticated API path instead.

Django's default password hasher (PBKDF2, or Argon2 if you install argon2-cffi) is solid. The failures here are usually process failures: log out on password change, rotate sessions on privilege escalation with cycle_key(), and rate-limit login endpoints with a package like django-axes to blunt credential stuffing.

Security headers and Content-Security-Policy

Django sets some headers, but a strong Content-Security-Policy is your best defense-in-depth against XSS. Add it explicitly:

CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)

A CSP that removes unsafe-inline means an injected script tag cannot execute even if it slips through. Verifying that these headers actually reach the browser in production is exactly the kind of check a dynamic application security test performs against the running app, catching a reverse-proxy override or a middleware ordering bug that no code review would surface.

The dependency layer

The Django parts of your risk are only half the picture. A typical Django project pulls in dozens of transitive packages: database drivers, image libraries, HTTP clients, and admin add-ons. A vulnerability in any of them is a vulnerability in your app.

Pin everything with hashes and audit continuously:

pip-compile --generate-hashes requirements.in
pip install -r requirements.txt --require-hashes
pip-audit -r requirements.txt

Hardening checklist

AreaControlHow to verify
SettingsDEBUG = False, secret in env, explicit ALLOWED_HOSTSmanage.py check --deploy
TransportSSL redirect, HSTS, secure cookiesExternal header scan
DatabaseORM only, params in .raw()Code review + SAST
TemplatesAuto-escape on, no untrusted mark_safeGrep for mark_safe/`
AuthArgon2, session rotation, login rate limitingAuth test suite
DependenciesHash-pinned, continuously auditedSCA in CI

How Safeguard helps

Safeguard is honest about the split: a scanner cannot fix a DEBUG = True for you, and we do not pretend to replace manage.py check --deploy. Where we add value is the dependency and runtime layer. Our software composition analysis ingests your requirements.txt or lockfile, maps the full transitive tree, and flags known CVEs in packages like pillow, psycopg, or Django itself the moment they are disclosed. When a fix exists, automated remediation opens a version-bump pull request with the changelog and reachability context attached, and you can run the same analysis locally before you push with the Safeguard CLI. The result is that the one dependency you forgot about does not become the way in.

Get started

Harden your settings today, then close the dependency gap. Create a free project at app.safeguard.sh/register and follow the Django integration walkthrough at docs.safeguard.sh.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.