Skip to content
Codeloom
Django

Django Production Security Checklist and Hardening

Harden your Django app for production — security settings, CSRF, XSS, SQL injection, HTTPS, headers, secrets, and deployment checks.

·7 min read · By Codeloom
Intermediate 12 min read

What you'll learn

  • How to run Django's built-in security check command
  • How to configure HTTPS, HSTS, and secure cookies
  • How Django protects against CSRF, XSS, and SQL injection
  • How to manage secrets and environment variables safely
  • How to set security headers and Content Security Policy

Prerequisites

  • A Django project ready for production deployment
  • Basic understanding of HTTP and web security concepts

Django ships with strong security defaults, but defaults aren’t enough for production. A misconfigured DEBUG = True, a leaked SECRET_KEY, or a missing HTTPS redirect can expose your entire application. This guide is a practical checklist — go through each section before every production deployment.

Start With Django’s Built-In Check

Django has a check --deploy command that scans your settings for common security misconfigurations:

python manage.py check --deploy

This reports issues like DEBUG = True, missing SECURE_SSL_REDIRECT, weak SECRET_KEY, and more. Fix every warning before deploying.

The Critical Settings

DEBUG = False

This is the single most important setting. With DEBUG = True:

  • Full tracebacks with source code are shown to users
  • The 404 page lists every URL pattern
  • Database queries are stored in memory (memory leak)
# settings.py
DEBUG = False  # NEVER True in production

SECRET_KEY From Environment

The SECRET_KEY signs sessions, CSRF tokens, and password reset links. If leaked, an attacker can forge any of these.

import os

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]  # Crash if missing, not silently use a default

Generate a strong key:

python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Never commit SECRET_KEY to version control. Use environment variables, a secrets manager (AWS Secrets Manager, HashiCorp Vault), or a .env file excluded from git.

ALLOWED_HOSTS

ALLOWED_HOSTS = ["example.com", "www.example.com"]

Never use ["*"] in production. This setting prevents HTTP Host header attacks. Without it, attackers can poison password reset emails and cache entries.

HTTPS and HSTS

Force HTTPS

SECURE_SSL_REDIRECT = True  # Redirect all HTTP to HTTPS
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")  # If behind a proxy/load balancer

HTTP Strict Transport Security

HSTS tells browsers to only use HTTPS for your domain, even if the user types http://:

SECURE_HSTS_SECONDS = 31536000  # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

Start with a short SECURE_HSTS_SECONDS (3600) and increase once you’ve confirmed everything works over HTTPS. HSTS is hard to undo — browsers cache it.

# Session cookie
SESSION_COOKIE_SECURE = True      # Only send over HTTPS
SESSION_COOKIE_HTTPONLY = True     # JavaScript can't read it
SESSION_COOKIE_SAMESITE = "Lax"   # Prevent CSRF via cross-site requests
SESSION_COOKIE_AGE = 1209600      # 2 weeks

# CSRF cookie
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = "Lax"

HTTPONLY prevents JavaScript from reading the cookie — mitigates XSS-based session theft. SAMESITE = "Lax" prevents the browser from sending cookies on cross-origin POST requests.

CSRF Protection

Django’s CSRF middleware is enabled by default. Keep it enabled and follow these rules:

MIDDLEWARE = [
    # ...
    "django.middleware.csrf.CsrfViewMiddleware",  # Don't remove this
    # ...
]

In templates, always use the {% csrf_token %} tag in forms:

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Save</button>
</form>

For AJAX requests, include the CSRF token in the header:

const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;

fetch("/api/data/", {
    method: "POST",
    headers: {
        "X-CSRFToken": csrftoken,
        "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
});

Never use @csrf_exempt on views unless you have a strong reason (like a webhook endpoint with its own authentication).

XSS Protection

Django’s template engine auto-escapes variables by default. The {{ variable }} syntax escapes HTML characters:

# If variable = '<script>alert("xss")</script>'
# Template renders: &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;

Never use {{ variable|safe }} or {% autoescape off %} on user-supplied content. If you must render HTML, sanitize it server-side with a library like bleach:

import bleach

ALLOWED_TAGS = ["p", "br", "strong", "em", "a", "ul", "ol", "li"]
ALLOWED_ATTRS = {"a": ["href", "title"]}

clean_html = bleach.clean(user_html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)

Enable the browser-side XSS protection header:

SECURE_BROWSER_XSS_FILTER = True  # Sets X-XSS-Protection header

SQL Injection Protection

Django’s ORM parameterizes all queries, preventing SQL injection by default:

# Safe — parameterized query
User.objects.filter(username=user_input)

# Safe — parameterized
User.objects.raw("SELECT * FROM auth_user WHERE username = %s", [user_input])

Never interpolate user input into raw SQL:

# DANGEROUS — SQL injection vulnerability
User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{user_input}'")

# DANGEROUS
cursor.execute(f"SELECT * FROM auth_user WHERE username = '{user_input}'")

If you must use raw SQL, always use parameterized queries with %s placeholders and pass values as a list.

Security Headers

Content-Type Sniffing

SECURE_CONTENT_TYPE_NOSNIFF = True  # Sets X-Content-Type-Options: nosniff

Prevents browsers from guessing the content type — stops attacks where an uploaded file is interpreted as HTML.

Clickjacking Protection

X_FRAME_OPTIONS = "DENY"  # Prevents your site from being embedded in iframes

Use "SAMEORIGIN" if you need iframes within your own site.

Content Security Policy

CSP is the most powerful XSS defense. Use django-csp:

pip install django-csp
# settings.py
MIDDLEWARE = [
    "csp.middleware.CSPMiddleware",
    # ...
]

CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")  # Tighten if possible
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com")
CSP_CONNECT_SRC = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)

Start with CSP_REPORT_ONLY = True to log violations without breaking your site, then switch to enforcement mode.

Password Security

Django’s password hashers are strong by default (PBKDF2 with 870,000 iterations as of Django 5.x). You can strengthen them:

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.Argon2PasswordHasher",  # Strongest, needs argon2-cffi
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
]

Install Argon2:

pip install argon2-cffi

Enforce password strength with validators:

AUTH_PASSWORD_VALIDATORS = [
    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", "OPTIONS": {"min_length": 10}},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]

File Upload Security

Uploaded files can be vectors for attack. Never trust the file extension or content type from the client:

# Validate file type server-side
import magic

def validate_file(uploaded_file):
    mime = magic.from_buffer(uploaded_file.read(2048), mime=True)
    uploaded_file.seek(0)

    allowed = ["image/jpeg", "image/png", "application/pdf"]
    if mime not in allowed:
        raise ValidationError(f"File type {mime} is not allowed")

Serve uploaded files from a separate domain or CDN to prevent cookie theft. Never serve user uploads from the same origin as your application.

# Use a separate storage backend
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
AWS_S3_CUSTOM_DOMAIN = "media.example.com"

Secrets Management

Never hardcode secrets. Use environment variables with a library like python-decouple or django-environ:

# settings.py
import environ

env = environ.Env()
environ.Env.read_env()  # reads .env file

SECRET_KEY = env("DJANGO_SECRET_KEY")
DATABASE_URL = env("DATABASE_URL")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
STRIPE_SECRET_KEY = env("STRIPE_SECRET_KEY")

Add .env to .gitignore:

# .gitignore
.env
*.env

Production Deployment Checklist

Run through this before every deployment:

# All of these should be set in production
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = ["yourdomain.com"]
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
SECURE_BROWSER_XSS_FILTER = True

Then run the deploy check:

python manage.py check --deploy

Fix every warning. Django’s security middleware does a lot of heavy lifting, but only if you configure it correctly.

Summary

Django’s security is strong out of the box, but production requires explicit configuration. Set DEBUG = False, load secrets from environment variables, enforce HTTPS with HSTS, secure your cookies, and add a Content Security Policy. Use python manage.py check --deploy as your gate before every release. Security isn’t a feature you add once — it’s a configuration you verify continuously.