Securing Django: Best Practices for 2018

python backend django security

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.

All posts