Django ships with a strong security foundation, but default settings are built for development convenience, not production safety. Every year, Django applications get compromised because developers forget to harden their configuration before going live. This checklist covers the ten most common vulnerabilities found in Django applications during security audits.
1. DEBUG = True in Production
This is the single most common Django misconfiguration. When DEBUG = True, Django displays detailed error pages that include your full stack trace, local variables, SQL queries, and the contents of your settings file — including your SECRET_KEY. Any unhandled exception exposes all of this to anyone who triggers it.
# settings/production.py
DEBUG = False
# Never commit DEBUG=True to production config
# Use environment variable: DEBUG = os.getenv('DEBUG', 'False') == 'True'
Run Django's built-in security check before every deploy: python manage.py check --deploy. It will flag DEBUG=True and several other issues from this list.
2. SECRET_KEY Exposure
The SECRET_KEY is used to sign cookies, CSRF tokens, password reset links, and session data. If an attacker obtains it, they can forge any of these. The most common way it leaks is through a public GitHub repository with the default settings.py committed.
# Wrong: hardcoded in settings.py
SECRET_KEY = 'django-insecure-abc123...'
# Correct: load from environment
import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
# Or use python-decouple:
from decouple import config
SECRET_KEY = config('SECRET_KEY')
Rotate your SECRET_KEY immediately if it has ever been committed to a public repository. All existing sessions and tokens will be invalidated — inform users if necessary.
3. SQL Injection via Raw Queries
Django's ORM uses parameterized queries by default and is safe from SQL injection when used correctly. The danger arises when developers use raw() or cursor.execute() with string formatting.
# Vulnerable — never do this
query = f"SELECT * FROM users WHERE username = '{username}'"
User.objects.raw(query)
# Safe — use parameterized queries
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])
# Even safer — use the ORM
User.objects.filter(username=username)
Grep your codebase for .raw(, cursor.execute(, and extra( calls. Review each one and ensure user input is always passed as a parameter, never interpolated into the query string.
4. Missing CSRF Middleware
Django includes CSRF protection via django.middleware.csrf.CsrfViewMiddleware. If this middleware is removed or if views are decorated with @csrf_exempt without good reason, POST endpoints become vulnerable to cross-site request forgery attacks.
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # Do not remove this
...
]
API endpoints that use token authentication (JWT, API keys) can legitimately exempt CSRF if they do not use cookie-based sessions. For all browser-facing views that use session cookies, CSRF protection is mandatory.
5. Clickjacking — Missing X-Frame-Options
Clickjacking attacks embed your application in an invisible iframe on a malicious site and trick users into clicking UI elements. Django prevents this with the XFrameOptionsMiddleware and the X_FRAME_OPTIONS setting.
MIDDLEWARE = [
...
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# settings.py
X_FRAME_OPTIONS = 'DENY'
# Use 'SAMEORIGIN' only if you need to embed your own pages in iframes
The default in modern Django is DENY, but double-check that the middleware is present and that no views override it with the @xframe_options_exempt decorator without documented justification.
6. Missing HTTPS Redirect (SECURE_SSL_REDIRECT)
If your application accepts HTTP connections, attackers can perform man-in-the-middle attacks to intercept or modify traffic. Django can automatically redirect all HTTP requests to HTTPS.
# settings/production.py
SECURE_SSL_REDIRECT = True
# If behind a load balancer or reverse proxy:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
Note: if you set SECURE_SSL_REDIRECT = True on a server that does not have TLS configured, your application will redirect into an infinite loop. Configure TLS first, then enable this setting.
7. HSTS Not Configured
HTTP Strict Transport Security tells browsers to only ever connect to your site over HTTPS, even if the user types http:// in the address bar. Without HSTS, users are vulnerable to SSL stripping attacks on their first visit.
# settings/production.py
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
Start with a short value (300 seconds) to test, then increase to 31536000 (one year) once you are confident your site will stay on HTTPS permanently. The PRELOAD flag allows your domain to be added to browser HSTS preload lists.
8. Insecure Session Configuration
Session cookies that lack proper flags can be stolen via XSS or transmitted over insecure connections. Django provides several settings to harden session cookies.
# settings/production.py
SESSION_COOKIE_SECURE = True # Only send over HTTPS
SESSION_COOKIE_HTTPONLY = True # Prevent JavaScript access (default True)
SESSION_COOKIE_SAMESITE = 'Strict' # Prevent CSRF via cookie
SESSION_COOKIE_AGE = 1209600 # 2 weeks — consider shorter for sensitive apps
# Same for CSRF cookie
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
9. ALLOWED_HOSTS Misconfiguration
With DEBUG = False, Django requires ALLOWED_HOSTS to be set to prevent HTTP Host header attacks. A common mistake is using a wildcard ('*') in production, which defeats the protection entirely.
# Wrong — never use wildcard in production
ALLOWED_HOSTS = ['*']
# Correct — list only your actual domains
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com', 'api.yourdomain.com']
# Load from environment for flexibility
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
10. Verbose Error Pages in Production
Even with DEBUG = False, applications can leak sensitive information through custom error handlers, logging misconfiguration, or third-party packages that print stack traces. Ensure your 404 and 500 templates do not expose internal paths, and configure logging to write errors to a file or external service rather than the HTTP response.
# settings/production.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': '/var/log/django/errors.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': True,
},
},
}
Running Django's Security Check
Django ships with a built-in deployment checklist: python manage.py check --deploy. Run it before every production deployment. It audits the most critical settings and prints a prioritized list of issues. A clean output is a prerequisite for production, not a guarantee of full security — but it catches the most common misconfigurations automatically.
Pair the Django check with an automated scanner like Shieldome to catch runtime issues (missing security headers, TLS configuration, exposed paths) that a static settings check cannot detect.