Securing Django: Best Practices for 2018
Django’s “batteries included” philosophy extends to security. The framework protects against many common vulnerabilities out of the box. But defaults aren’t enough—here’s how to harden your Django application for production.
Enable HTTPS Everywhere
HTTPS is non-negotiable. Here’s your checklist:
Get a Certificate
Let’s Encrypt provides free certificates. Use certbot:
sudo certbot --nginx -d yourdomain.com
Django Settings
# settings.py (production)
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
HSTS (HTTP Strict Transport Security)
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
HSTS tells browsers to always use HTTPS. Start with a short duration, then increase once confirmed working.
Protect Sensitive Settings
Never Commit Secrets
# BAD - secrets in code
SECRET_KEY = 'django-insecure-abc123...'
DATABASE_PASSWORD = 'mypassword'
# GOOD - environment variables
import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DATABASE_PASSWORD = os.environ['DB_PASSWORD']
Use python-decouple or django-environ
from decouple import config
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
DATABASE_URL = config('DATABASE_URL')
Generate Strong SECRET_KEY
from django.core.management.utils import get_random_secret_key
print(get_random_secret_key())
Security Headers
Content Security Policy
# settings.py
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", 'cdn.example.com')
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", 'data:', 'cdn.example.com')
Use django-csp to manage Content-Security-Policy headers.
Additional Headers
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
CSRF Protection
Django’s CSRF protection is enabled by default. Keep it that way.
# In templates
<form method="post">
{% csrf_token %}
...
</form>
# In AJAX requests
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
fetch('/api/endpoint/', {
method: 'POST',
headers: {
'X-CSRFToken': csrftoken,
},
body: JSON.stringify(data)
});
Never use @csrf_exempt without careful consideration.
SQL Injection Prevention
Django’s ORM protects you, but be careful with raw SQL:
# SAFE - parameterized queries
User.objects.raw('SELECT * FROM users WHERE id = %s', [user_id])
# DANGEROUS - string formatting
User.objects.raw(f'SELECT * FROM users WHERE id = {user_id}') # NO!
The ORM’s queryset API is always safe:
User.objects.filter(id=user_id) # Always safe
User.objects.filter(name__icontains=search_term) # Always safe
XSS Prevention
Django’s template engine auto-escapes by default:
<!-- Safe - auto-escaped -->
{{ user_input }}
<!-- Dangerous - only when you trust the content -->
{{ trusted_html|safe }}
<!-- For rich text, use bleach to sanitize -->
{{ user_html|bleach }}
When you need safe, sanitize first:
import bleach
clean_html = bleach.clean(
user_html,
tags=['p', 'br', 'strong', 'em'],
strip=True
)
Authentication Best Practices
Password Validation
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': 12}
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
Session Security
SESSION_COOKIE_AGE = 1209600 # 2 weeks
SESSION_COOKIE_SECURE = True # HTTPS only
SESSION_COOKIE_HTTPONLY = True # No JavaScript access
SESSION_EXPIRE_AT_BROWSER_CLOSE = True # Optional
Rate Limiting Login Attempts
Use django-axes or django-defender:
INSTALLED_APPS = ['axes', ...]
AUTHENTICATION_BACKENDS = [
'axes.backends.AxesBackend',
'django.contrib.auth.backends.ModelBackend',
]
AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 1 # hours
File Upload Security
# Limit upload size
DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
# Validate file types
def validate_file_extension(value):
valid_extensions = ['.pdf', '.doc', '.docx']
ext = os.path.splitext(value.name)[1]
if ext.lower() not in valid_extensions:
raise ValidationError('Invalid file type')
# Store uploads outside web root
MEDIA_ROOT = '/var/www/uploads/' # Not in static files
Run Security Checks
Django includes a security check command:
python manage.py check --deploy
This flags common security issues in your configuration.
Production Checklist
# settings/production.py
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# Security
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
Monitoring and Logging
LOGGING = {
'version': 1,
'handlers': {
'security': {
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/var/log/django/security.log',
},
},
'loggers': {
'django.security': {
'handlers': ['security'],
'level': 'WARNING',
},
},
}
Monitor for suspicious patterns and set up alerts.
Final Thoughts
Security is a process, not a checklist. These practices form a solid foundation, but stay informed about new vulnerabilities and update dependencies regularly.
Django makes security easier than most frameworks. Take advantage of that foundation, and build on it carefully.
Defense in depth. Trust nothing.