Security features and hardening

Django · Advanced · 15 min

Django is secure by default only if you keep its protections on and configure the production settings.

Django defends against the classic attacks, as long as you don't bypass it:

  • SQL injection: the ORM parameterizes queries. With raw() or cursor.execute(), pass values as params, never with f-strings.
  • XSS: templates autoescape. |safe, mark_safe and {% autoescape off %} disable it; build HTML in Python with format_html(), which escapes arguments.
  • CSRF: CsrfViewMiddleware requires a token on unsafe methods; include {% csrf_token %} in forms and send the X-CSRFToken header from JavaScript. Avoid @csrf_exempt except for endpoints authenticated by other means.
  • Clickjacking: XFrameOptionsMiddleware sends X-Frame-Options: DENY.
  • Host header attacks: ALLOWED_HOSTS.

HTTPS settings for production:

  • SECURE_SSL_REDIRECT = True
  • SESSION_COOKIE_SECURE = True and CSRF_COOKIE_SECURE = True
  • SECURE_HSTS_SECONDS (start small), SECURE_HSTS_INCLUDE_SUBDOMAINS
  • SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") behind a proxy that sets it

Content Security Policy is built in since Django 6.0: add django.middleware.csp.ContentSecurityPolicyMiddleware and set SECURE_CSP (or SECURE_CSP_REPORT_ONLY to test first) using constants from django.utils.csp.CSP. With CSP.NONCE in a directive and the csp context processor, templates add nonce="{{ csp_nonce }}" to inline scripts.

Also: never run with DEBUG=True in production, keep Django updated, use the strongest password hasher you can afford, and run python manage.py check --deploy in CI.

Example

# settings.py (production)
from django.utils.csp import CSP

DEBUG = False
ALLOWED_HOSTS = ["blog.example.com"]
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 3600                 # raise to a year once verified
MIDDLEWARE.insert(1, "django.middleware.csp.ContentSecurityPolicyMiddleware")
SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "img-src": [CSP.SELF, "https:"],
}
TEMPLATES[0]["OPTIONS"]["context_processors"].append("django.template.context_processors.csp")

# Safe raw SQL and HTML building
from django.db import connection
from django.utils.html import format_html
def search(term):
    with connection.cursor() as cur:
        cur.execute("SELECT id FROM blog_post WHERE title LIKE %s", [f"%{term}%"])   # params!
        return cur.fetchall()
link = format_html('<a href="{}">{}</a>', "/u/1", "<script>")  # <a href="/u/1">&lt;script&gt;</a>

Remember

  • ORM and params prevent SQL injection; never interpolate SQL strings
  • Autoescaping, CSRF tokens and X-Frame-Options are on by default; don't bypass them
  • Enable HTTPS redirects, secure cookies and HSTS in production
  • Django 6.0 has built-in CSP via middleware and SECURE_CSP

Watch out for

  • Marking user content with mark_safe or |safe to "fix" escaped HTML.

Check yourself

  1. Which raw query is safe from SQL injection?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/topics/security/