Django Security Best Practices
Introduction
Django defaults are security-conscious—CSRF middleware, password hashing, template auto-escaping—but production security depends on your settings, deployment, and habits. This chapter consolidates checklist items: secrets, DEBUG, HTTPS, CSRF/XSS/SQL injection defenses, headers, dependency audits, and login protection—building on Settings, Forms and CSRF, and API auth.
Prerequisites
- Settings and Multi-Environment
- Forms and CSRF
- Static and Media in Production — upload safety
- Planned deploy: Gunicorn and Nginx
Production Settings Checklist
config/settings/production.py essentials:
| Setting | Purpose |
|---|---|
DEBUG=False | Hide tracebacks and settings |
SECRET_KEY from env | Signing sessions/CSRF |
ALLOWED_HOSTS | Reject wrong Host header |
SECURE_SSL_REDIRECT | HTTP → HTTPS |
SECURE_*_COOKIE | Cookies only over TLS |
HSTS | Browser remembers HTTPS |
Terminate TLS at Nginx — HTTPS and Let's Encrypt.
SECRET_KEY and Secrets
- Generate unique key per environment
- Never commit
.envor production settings with literals - Rotate key if leaked (invalidates all sessions)
- Database passwords, API keys via
os.environ
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"CSRF
- All browser POST forms:
{% csrf_token %} - AJAX with session auth:
X-CSRFTokenheader - APIs: Token/JWT instead of disabling CSRF globally
@csrf_exempt: webhooks only, with signature verification
See Forms and CSRF.
XSS (Cross-Site Scripting)
Django templates escape {{ variable }} by default.
| Safe | Risky |
|---|---|
{{ user_comment }} | {{ user_comment|safe }} on untrusted input |
mark_safe() on trusted admin HTML only | Building HTML with string concat from POST |
JSON APIs: DRF JSON encoder escapes strings in responses; XSS hits when frontend uses innerHTML with API data—sanitize in client or use text nodes.
Content-Security-Policy (CSP) header via middleware or Nginx—restrict script sources for high-security apps.
SQL Injection
Django ORM parameterizes queries:
Post.objects.filter(title=user_input) # safeDangerous:
Post.objects.raw(f"SELECT * FROM blog_post WHERE title = '{user_input}'") # neverUse ORM or cursor.execute(sql, [params]) with placeholders.
Clickjacking
XFrameOptionsMiddleware sets X-Frame-Options: DENY by default. Allow embedding only when intentional:
from django.views.decorators.clickjacking import xframe_options_exemptUser Uploads
From Static and Media:
- Validate extension and MIME type
- Limit file size in form and Nginx
client_max_body_size - Store outside executable paths; serve with
Content-Disposition: attachmentfor downloads when appropriate - Scan with antivirus in high-risk domains (awareness)
Authentication Hardening
AUTH_PASSWORD_VALIDATORS (default enabled):
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"},
]Additional measures:
| Measure | Tool / approach |
|---|---|
| Login rate limit | django-axes, Nginx limit_req, fail2ban |
| Admin URL obscurity | Non-default path + IP allowlist |
| 2FA for admin | django-otp |
| Lock inactive accounts | is_active=False |
Protect /admin/ — Django Admin security.
API Security
DEFAULT_PERMISSION_CLASSES: notAllowAnyfor writes in production- Object-level permissions on update/delete
- JWT: short access TTL, refresh rotation, HTTPS only
- Do not expose stack traces in JSON errors
CORS
- Never
CORS_ALLOW_ALL_ORIGINS = Truewith credentials in production - Whitelist exact frontend origins
- Prefer same-origin Nginx proxy for SPAs
Dependency Scanning
pip auditOr pip-audit, GitHub Dependabot, Snyk. Pin versions in requirements.txt; update Django for security releases promptly.
Logging and Monitoring
- Log
django.request5xx atERROR - Do not log passwords, tokens, or full
Authorizationheaders - Alert on error rate spikes — Error Handling and Logging
- Sentry for exception tracking (optional)
Security Middleware Order
Keep Django defaults unless you know why:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
...
]SecurityMiddleware applies SECURE_* settings.
runserver and DEBUG in Production
| Never in production | Use instead |
|---|---|
manage.py runserver | Gunicorn + Nginx |
DEBUG=True | DEBUG=False + logging |
| SQLite for high traffic | MySQL/PostgreSQL |
Default SECRET_KEY from tutorial | Env-generated key |
Pre-Deploy Security Command
DJANGO_SETTINGS_MODULE=config.settings.production python manage.py check --deployFixes warnings before go-live.
Common Vulnerabilities Summary
| Threat | Django defense | Your responsibility |
|---|---|---|
| CSRF | Middleware + token | Forms and AJAX headers |
| XSS | Template escape | Avoid |safe on user data |
| SQL injection | ORM | No raw string SQL |
| Session hijack | HTTPS, secure cookies | TLS everywhere |
| Host header attack | ALLOWED_HOSTS | Set correctly |
| Clickjacking | X-Frame-Options | Do not exempt blindly |
Post-Chapter Checklist
- Production settings:
DEBUG=False, envSECRET_KEY,ALLOWED_HOSTS - HTTPS and secure cookie flags configured
-
check --deploypasses - CSRF on forms; API auth on writes
-
pip auditrun on dependencies - Admin and uploads hardened
FAQ## FAQ
Django secure by default?
Mostly for CSRF, XSS in templates, SQL via ORM—but DEBUG, SECRET_KEY, HTTPS, and permissions are on you.
Security through obscurity?
Hiding /admin/ helps noise reduction only—use strong auth and network controls.
Content-Security-Policy?
Add via django-csp or Nginx—reduces XSS impact.
GDPR / privacy?
Cookie consent, data export/delete—legal/process; User model holds PII.
Penetration testing?
Run check --deploy, OWASP ZAP on staging, fix findings before prod.