Security Intermediate

Django Security Best Practices Checklist

Essential security practices for Django applications. Covers CSRF, XSS, SQL injection, HTTPS, headers, authentication, and deployment hardening.

DjangoZen Team Mar 29, 2026 4 min read 27 views

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.

1. Keep SECRET_KEY Secret

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')
If your SECRET_KEY was ever committed to Git: Generate a new one immediately. Old sessions will be invalidated, and users will need to log in again. That's OK — it's better than a compromised key.

2. CSRF Protection

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')
}

3. SQL Injection Prevention

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)

4. XSS Prevention

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 -->

5. Authentication Security

# 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

6. HTTPS and Security Headers

# 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

7. File Upload Security

# 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

8. Rate Limiting

# 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)
    ...

Security Checklist

ItemPriorityDjango Setting
DEBUG = FalseCriticalDEBUG
SECRET_KEY from envCriticalSECRET_KEY
HTTPS enabledCriticalSECURE_SSL_REDIRECT
CSRF protection activeCriticalMiddleware enabled by default
Secure cookiesHighSESSION_COOKIE_SECURE
HSTS headersHighSECURE_HSTS_SECONDS
Rate limiting on loginHighdjango-ratelimit or custom
File upload validationHighCustom validators
Dependencies updatedMediumpip list --outdated
Admin URL changedMediumpath('secret-admin/',...)
# Run Django's built-in security check
python manage.py check --deploy
Stay updated: Subscribe to the Django security mailing list and update Django regularly. Security patches are released as needed and should be applied immediately.

Summary

  • Django has strong security defaults — don't disable them
  • Keep secrets out of source code (use environment variables)
  • Always use HTTPS in production with proper headers
  • Use the ORM instead of raw SQL to prevent injection
  • Be careful with |safe filter — it disables XSS protection
  • Rate limit login and sensitive endpoints
  • Run manage.py check --deploy before every deployment