Essential security practices for Django applications. Covers CSRF, XSS, SQL injection, HTTPS, headers, authentication, and deployment hardening.
Django has excellent built-in security features, but they only work if you use them correctly. This checklist covers the essential security practices every Django developer should follow to protect their application and users.
The SECRET_KEY is used for cryptographic signing (sessions, CSRF tokens, password reset links). If it's exposed, attackers can forge any of these.
# BAD - hardcoded in settings.py (committed to git)
SECRET_KEY = 'django-insecure-abc123...'
# GOOD - from environment variable
import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
# GOOD - from .env file with python-decouple
from decouple import config
SECRET_KEY = config('SECRET_KEY')
Django's CSRF middleware prevents attackers from submitting forms on behalf of your users:
# In every POST form:
<form method="post">
{% csrf_token %}
<!-- form fields -->
<button type="submit">Submit</button>
</form>
# For AJAX requests, include the token in headers:
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
Django's ORM automatically parameterizes queries. But raw SQL can be dangerous:
# BAD - SQL injection vulnerability!
User.objects.raw(f"SELECT * FROM users WHERE name = '{user_input}'")
# GOOD - parameterized query
User.objects.raw("SELECT * FROM users WHERE name = %s", [user_input])
# BEST - use the ORM
User.objects.filter(name=user_input)
Django auto-escapes template variables. Be careful with the safe filter:
<!-- SAFE - auto-escaped -->
{{ user_comment }}
<!-- DANGEROUS - only use for trusted HTML -->
{{ user_comment|safe }} <!-- Could contain <script> tags! -->
<!-- GOOD - sanitize first if you need HTML -->
<!-- Use bleach or django-bleach to whitelist allowed tags -->
# settings.py
# Strong password validation
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'},
]
# Session security
SESSION_COOKIE_AGE = 3600 # 1 hour
SESSION_COOKIE_SECURE = True # HTTPS only
SESSION_COOKIE_HTTPONLY = True # No JS access
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
# settings.py - Production security settings
# Force HTTPS
SECURE_SSL_REDIRECT = True
# HSTS - tell browsers to always use HTTPS
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Prevent clickjacking
X_FRAME_OPTIONS = 'DENY'
# Prevent MIME type sniffing
SECURE_CONTENT_TYPE_NOSNIFF = True
# Cookies
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
# Restrict file types
import os
from django.core.exceptions import ValidationError
def validate_file_type(upload):
allowed = ['.pdf', '.jpg', '.png', '.zip']
ext = os.path.splitext(upload.name)[1].lower()
if ext not in allowed:
raise ValidationError('File type not allowed.')
# Limit file size
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 # 10 MB
# Never serve uploaded files from the same domain
# Use a CDN or separate subdomain for user uploads
# Using django-ratelimit
pip install django-ratelimit
from django_ratelimit.decorators import ratelimit
@ratelimit(key='ip', rate='5/m') # 5 attempts per minute
def login_view(request):
if getattr(request, 'limited', False):
return HttpResponse('Too many attempts. Try again later.', status=429)
...
| Item | Priority | Django Setting |
|---|---|---|
| DEBUG = False | Critical | DEBUG |
| SECRET_KEY from env | Critical | SECRET_KEY |
| HTTPS enabled | Critical | SECURE_SSL_REDIRECT |
| CSRF protection active | Critical | Middleware enabled by default |
| Secure cookies | High | SESSION_COOKIE_SECURE |
| HSTS headers | High | SECURE_HSTS_SECONDS |
| Rate limiting on login | High | django-ratelimit or custom |
| File upload validation | High | Custom validators |
| Dependencies updated | Medium | pip list --outdated |
| Admin URL changed | Medium | path('secret-admin/',...) |
# Run Django's built-in security check
python manage.py check --deploy
|safe filter — it disables XSS protectionmanage.py check --deploy before every deployment