Best Practices

Django Security Best Practices, 2024 Edition

From SECRET_KEY hygiene to middleware ordering, the Django security checklist worth actually following in 2024, grounded in real CVEs and production incidents.

Shadab Khan
Security Engineer
6 min read

Django turned nineteen this year and it still runs a remarkable slice of the Python web. Instagram, Pinterest, the NHS COVID app, a long tail of fintech back ends, the internal admin consoles for half the startups I talk to. The framework has a justified reputation for safe defaults, but the defaults only carry you so far. Every release cycle I see the same mistakes in code review: settings files that leak secrets, middleware stacked in the wrong order, ORM queries reaching for raw() when a filter would do.

This is the 2024 version of a checklist I have been maintaining for my team. It leans on real CVEs and real postmortems rather than general hand-waving.

SECRET_KEY Is Not a Password, It Is a Signing Key

The SECRET_KEY setting is used for session signing, CSRF tokens, password reset tokens, and the django.core.signing module. If it leaks, an attacker can forge sessions for arbitrary users. And yet every year the Django security team receives reports of keys checked into public GitHub repos, usually because a developer copied the startproject default and never rotated it.

Two concrete steps. First, load it from the environment or a secret manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager), never from settings.py. Second, when you rotate, use SECRET_KEY_FALLBACKS (added in Django 4.1) so existing sessions do not get invalidated mid-rotation. I have seen teams skip rotation for years because they were scared of the logout storm, which is exactly backwards.

GitHub's secret scanning catches some Django keys but not all of them, because the default format is just fifty random characters and has no distinguishing prefix. Do not rely on it.

Are You Actually Safe from SQL Injection?

Django's ORM is excellent but it is not magic. The moment you reach for raw(), extra(), RawSQL, or .annotate() with unsanitized user input, you are back to writing SQL by hand and all the old rules apply.

CVE-2024-27351 is a recent reminder. The urlize and urlizetrunc template filters had a regex-based denial-of-service, not an injection, but the root cause was the same pattern I see everywhere: trusting that a framework helper is bulletproof because it is shipped with the framework. It is worth running Bandit across your codebase with the B611 check enabled, which flags RawSQL usage.

For the ORM itself, be cautious with .extra(where=[...]) — the where argument is not parameterized unless you pass params explicitly. I have lost hours to a junior engineer who thought Django would sanitize a user-supplied column name in order_by.

Middleware Order Matters More Than People Think

The MIDDLEWARE setting is a stack. SecurityMiddleware should be first or near-first, so that HSTS and SSL redirect happen before anything else touches the request. SessionMiddleware has to come before AuthenticationMiddleware, which has to come before any middleware that expects request.user. CsrfViewMiddleware needs to be before any view that renders forms.

I keep seeing django-cors-headers placed below CommonMiddleware, which means CORS headers do not get set on redirects. The project's own README says to put CorsMiddleware as high as possible, above WhiteNoise and CommonMiddleware. Read the READMEs of your third-party middleware. This is not optional.

Upgrading Is a Security Control

Django ships security releases roughly monthly. Between January 2023 and March 2024, the project published advisories for CVE-2023-31047 (file upload validation bypass), CVE-2023-36053 (EmailValidator ReDoS), CVE-2023-41164 (potential DoS in django.utils.encoding.uri_to_iri), and CVE-2024-24680 (DoS in intcomma template filter with very large numbers). None are catastrophic on their own. Together they represent a steady drip of issues that only patched Django handles safely.

The long-term support releases (4.2 LTS through April 2026, 5.2 LTS from April 2025) exist so you can stay patched without tracking every feature release. If you are still on Django 3.2, you are running software that stopped receiving security fixes in April 2024. Upgrade.

CSRF, Cookies, and the Subtle Cases

Django's CSRF protection works well if you leave it alone. The cases where I see it get disabled are almost always wrong: a webhook endpoint that uses @csrf_exempt without any other form of authentication, an API that disabled CSRF because "we use tokens" but serves the token from a cookie readable by JavaScript.

For cookies, set SESSION_COOKIE_SECURE = True, CSRF_COOKIE_SECURE = True, SESSION_COOKIE_HTTPONLY = True (the default), and SESSION_COOKIE_SAMESITE = 'Lax' or 'Strict'. If you are using Django's authentication with a single-page front end, SameSite=Lax is usually what you want, because Strict breaks OAuth redirects.

The less obvious setting is CSRF_TRUSTED_ORIGINS. Since Django 4.0 this requires the scheme, so https://example.com not just example.com. Teams that upgraded from 3.2 to 4.x without reading the release notes discovered this when their admin forms started returning 403.

Deploy-Time Checks Are Free

Run python manage.py check --deploy as part of your CI pipeline. It flags insecure settings: DEBUG=True in production, missing SECURE_HSTS_SECONDS, weak session cookie configuration. It is a five-second job and it catches the bulk of the "oops we deployed with dev settings" incidents.

Pair it with django-extensions' show_urls so you can audit which URLs are exposed. I have seen a /debug/ endpoint from a third-party package surface in production because nobody knew it was registered.

Admin Hardening Is Not Optional

/admin is a login page that advertises itself as a login page. Brute force against Django admin is a daily occurrence on anything public. Three mitigations, in order of impact.

First, put the admin behind something. A VPN, Cloudflare Access, a non-default URL, an IP allowlist. Second, use django-axes or django-defender for rate limiting and lockout. Third, require MFA for staff users — django-mfa2 or django-otp both work, and django-allauth has decent MFA support if you are already using it.

Template Auto-Escaping Has Edge Cases

Django templates auto-escape by default, which is why XSS in Django apps is rarer than in, say, raw Jinja without autoescape. But |safe, {% autoescape off %}, and mark_safe() are all escape hatches that show up too often. CVE-2021-44420 (the django.contrib.admindocs XSS) was an internal use of mark_safe on user-controllable data, and Django itself had to patch it.

Audit every |safe in your templates. If it is applied to anything touched by user input, it is a bug.

How Safeguard Helps

Safeguard's reachability analysis tells you which Django CVEs actually touch code paths your app exercises, so a ReDoS in urlize that you never call is not a 3 AM page. Griffin AI drafts the upgrade PR from Django 4.2 to the next LTS, reading your requirements.txt, your settings file, and the Django release notes to surface breaking changes specific to your middleware stack. SBOM generation captures the full dependency tree including the admin themes and contrib packages people forget about. Policy gates can block a deploy when DEBUG=True or ALLOWED_HOSTS=['*'] slips through, and they can enforce that SECURE_HSTS_SECONDS is set before any production release tag is cut.

Never miss an update

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