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
| Area | Control | How to verify |
|---|---|---|
| Settings | DEBUG = False, secret in env, explicit ALLOWED_HOSTS | manage.py check --deploy |
| Transport | SSL redirect, HSTS, secure cookies | External header scan |
| Database | ORM only, params in .raw() | Code review + SAST |
| Templates | Auto-escape on, no untrusted mark_safe | Grep for mark_safe/` |
| Auth | Argon2, session rotation, login rate limiting | Auth test suite |
| Dependencies | Hash-pinned, continuously audited | SCA 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.