Django security best practices. Covers CSRF, XSS, SQL injection prevention, authentication hardening, Content Security Policy, secrets management, and Django-specific security middleware configuration.
Install
npx skillscat add peopleforrester/claude-dotfiles/django-security Install via the SkillsCat registry.
SKILL.md
Django Security Patterns
Security hardening for Django 5.x applications.
Settings Hardening
# config/settings/production.py
# HTTPS enforcement
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Cookie security
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
# Content security
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = "DENY"
# Host validation
ALLOWED_HOSTS = [".example.com"]
# Secret key from environment
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # Never hardcodeCSRF Protection
# All POST forms require CSRF token
# Template:
# {% csrf_token %}
# For API views using DRF, CSRF is handled via authentication classes
from rest_framework.authentication import SessionAuthentication
class CsrfExemptSessionAuth(SessionAuthentication):
"""Only use for webhook endpoints with signature verification."""
def enforce_csrf(self, request):
return # Skip CSRF for verified webhooks only
# Verify webhook signatures instead of skipping CSRF blindly
import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)Input Validation
from django.core.validators import RegexValidator
from django.db import models
class User(models.Model):
# Constrain input at model level
username = models.CharField(
max_length=30,
validators=[RegexValidator(r"^[a-zA-Z0-9_]+$", "Alphanumeric and underscores only")],
)
email = models.EmailField(unique=True)
# DRF serializer validation
from rest_framework import serializers
class UserInputSerializer(serializers.Serializer):
username = serializers.RegexField(r"^[a-zA-Z0-9_]+$", max_length=30)
email = serializers.EmailField()
def validate_username(self, value):
if value.lower() in {"admin", "root", "system"}:
raise serializers.ValidationError("Reserved username.")
return valueSQL Injection Prevention
# Always use ORM or parameterized queries
# Good: ORM handles parameterization
users = User.objects.filter(name=user_input)
# Good: Parameterized raw SQL when ORM insufficient
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM users WHERE name = %s", [user_input])
# NEVER: String interpolation in SQL
# cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'") # SQL INJECTIONAuthentication Hardening
# Custom authentication backend with rate limiting
from django.contrib.auth.backends import ModelBackend
from django.core.cache import cache
class RateLimitedAuthBackend(ModelBackend):
MAX_ATTEMPTS = 5
LOCKOUT_SECONDS = 300 # 5 minutes
def authenticate(self, request, username=None, password=None, **kwargs):
cache_key = f"login_attempts:{username}"
attempts = cache.get(cache_key, 0)
if attempts >= self.MAX_ATTEMPTS:
return None # Account locked
user = super().authenticate(request, username=username, password=password, **kwargs)
if user is None:
cache.set(cache_key, attempts + 1, self.LOCKOUT_SECONDS)
else:
cache.delete(cache_key)
return user
# Password validation in settings
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"},
]Content Security Policy
# Using django-csp
# pip install django-csp
MIDDLEWARE = [
"csp.middleware.CSPMiddleware",
# ...
]
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'") # Tighten if possible
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_FONT_SRC = ("'self'",)
CSP_CONNECT_SRC = ("'self'",)
CSP_FRAME_SRC = ("'none'",)
CSP_OBJECT_SRC = ("'none'",)XSS Prevention
# Django auto-escapes template variables by default
# {{ user_input }} -> escaped
# {{ user_input|safe }} -> DANGEROUS, only use for trusted HTML
# Sanitize rich text with bleach
import bleach
ALLOWED_TAGS = ["p", "b", "i", "a", "ul", "ol", "li", "br"]
ALLOWED_ATTRS = {"a": ["href", "title"]}
def sanitize_html(html_input: str) -> str:
return bleach.clean(html_input, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, strip=True)Secrets Management
import os
from pathlib import Path
# Environment-based secrets (production)
DATABASE_URL = os.environ["DATABASE_URL"]
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
API_KEY = os.environ["EXTERNAL_API_KEY"]
# For local development, use .env file with django-environ
# pip install django-environ
import environ
env = environ.Env()
environ.Env.read_env(Path(__file__).resolve().parent / ".env")
SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env.bool("DEBUG", default=False)
DATABASE_URL = env.db("DATABASE_URL")File Upload Security
import os
import uuid
from django.core.exceptions import ValidationError
def validate_file_extension(value):
ext = os.path.splitext(value.name)[1].lower()
allowed = {".jpg", ".jpeg", ".png", ".gif", ".pdf"}
if ext not in allowed:
raise ValidationError(f"Unsupported file extension: {ext}")
def upload_to(instance, filename):
"""Generate random filename to prevent path traversal."""
ext = os.path.splitext(filename)[1].lower()
return f"uploads/{uuid.uuid4().hex}{ext}"
class Document(models.Model):
file = models.FileField(
upload_to=upload_to,
validators=[validate_file_extension],
max_length=255,
)Security Middleware Stack
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", # Must be first
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"csp.middleware.CSPMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
# ... application middleware
]Dependency Auditing
# Check for known vulnerabilities
pip-audit
# or
safety check
# Pin dependencies with hashes
pip-compile --generate-hashes requirements.in
pip install --require-hashes -r requirements.txtChecklist
-
SECRET_KEYloaded from environment, never committed - HTTPS enforced with HSTS in production
- CSRF protection enabled on all state-changing endpoints
- SQL queries use ORM or parameterized raw SQL only
- User input validated at serializer/form level
- File uploads use random filenames and extension validation
- Rate limiting on authentication endpoints
- CSP headers configured
- Dependencies audited for known vulnerabilities
- Session cookies marked Secure, HttpOnly, SameSite