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()orcursor.execute(), pass values asparams, never with f-strings. - XSS: templates autoescape.
|safe,mark_safeand{% autoescape off %}disable it; build HTML in Python withformat_html(), which escapes arguments. - CSRF:
CsrfViewMiddlewarerequires a token on unsafe methods; include{% csrf_token %}in forms and send theX-CSRFTokenheader from JavaScript. Avoid@csrf_exemptexcept for endpoints authenticated by other means. - Clickjacking:
XFrameOptionsMiddlewaresendsX-Frame-Options: DENY. - Host header attacks:
ALLOWED_HOSTS.
HTTPS settings for production:
SECURE_SSL_REDIRECT = TrueSESSION_COOKIE_SECURE = TrueandCSRF_COOKIE_SECURE = TrueSECURE_HSTS_SECONDS(start small),SECURE_HSTS_INCLUDE_SUBDOMAINSSECURE_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"><script></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.