Django protects against most common web attacks out of the box, but a single production setting such as DEBUG = True, a wildcard ALLOWED_HOSTS or a leaked SECRET_KEY can undo that protection for the whole site. Many Django incidents trace back to settings, to code that leaves the ORM for raw SQL or unsafe dictionary expansion, or to forms that expose more model fields than intended. This guide covers the settings that matter in production and how manage.py check --deploy enforces them, safe ORM and template patterns, sessions and CSRF, secret key handling, file uploads and the new built-in Content Security Policy. It is for Django developers, application security engineers and platform teams running Django services, and it complements the language-level Python Security Guide rather than repeating it.
Key takeaways
- Run check --deploy against production settings. It flags missing
SecurityMiddleware, weakSECRET_KEYvalues,DEBUG, emptyALLOWED_HOSTS, HSTS, cookie and CSP problems by check ID, and it belongs in CI as a failing gate. - The ORM is safe until you step outside it. Querysets are parameterised;
raw(),extra(),RawSQL,cursor.executewith string formatting and**request.GETexpansion intofilter()are where injection returns, as CVE-2025-64459 showed. - SECRET_KEY signs everything that matters. Sessions, password reset tokens, messages and signed cookies depend on it; load it from the environment or a secrets manager and rotate with
SECRET_KEY_FALLBACKS. - Explicit fields stop mass assignment. Django's own documentation recommends listing
fieldson everyModelFormand calls"__all__"andexcludemuch less secure. - Some protection is not in Django at all. Login throttling, CORS, upload size limits at the web server and object-level authorisation are your responsibility.
Built-in protections and their limits
The Django security overview is explicit that its defaults suit general-purpose sites and that special cases need attention. The table lists each built-in protection, the component that provides it and the documented way it fails.
| Protection | Provided by | Default | Documented limit or bypass |
|---|---|---|---|
| XSS | Template autoescaping | On | |safe, mark_safe, is_safe custom filters, {% autoescape off %}, unquoted attributes such as class={{ var }}, non-HTML output |
| CSRF | CsrfViewMiddleware, {% csrf_token %} | On in the default MIDDLEWARE | @csrf_exempt, untrusted subdomains, state changes on GET |
| SQL injection | QuerySet parameterisation | On | raw(), extra(), RawSQL, formatted cursor.execute strings, quoted %s placeholders |
| Clickjacking | XFrameOptionsMiddleware | X_FRAME_OPTIONS = 'DENY' | Per-view exemptions, middleware removed |
| Host header | HttpRequest.get_host() with ALLOWED_HOSTS | Enforced when DEBUG = False | Reading request.META["HTTP_HOST"] directly, wildcard hosts, careless USE_X_FORWARDED_HOST |
| Passwords | django.contrib.auth hashers | PBKDF2 with SHA256 | Custom auth that stores or compares passwords itself |
| Security headers | SecurityMiddleware | nosniff on, Referrer-Policy: same-origin, Cross-Origin-Opener-Policy: same-origin; HSTS and SSL redirect off | Middleware missing (check security.W001) |
| Content Security Policy | ContentSecurityPolicyMiddleware (Django 6.0 and later) | Off until SECURE_CSP is set | Excluded paths weaken the whole site |
Django does not throttle authentication attempts and does not ship CORS support. Add a throttling package or web server rule for login, password reset and API token endpoints, and use a maintained CORS middleware with an explicit origin list. See the OWASP Top 10 Guide for the wider vulnerability classes.
Production settings and deployment checks
python manage.py check --deploy, run with the production settings module, turns the deployment checklist into machine-checkable warnings. The table maps the settings that matter to their check IDs from django/core/checks/security.
| Setting | Production value | Check ID | Notes |
|---|---|---|---|
DEBUG | False | security.W018 | True leaks source excerpts, local variables and settings in error pages |
SECRET_KEY | Long random value from the environment or a file | security.W009 | Warns under 50 characters, under 5 unique characters or with the django-insecure- prefix; W025 applies the same test to fallbacks |
ALLOWED_HOSTS | Explicit host names | security.W020 | Also reject unknown hosts at the proxy, for example an nginx default server returning 444 |
MIDDLEWARE | Includes SecurityMiddleware and XFrameOptionsMiddleware | security.W001, W002 | Without SecurityMiddleware the HSTS, nosniff, referrer, COOP and redirect settings do nothing |
SECURE_SSL_REDIRECT | True, or redirect at the proxy | security.W008 | Behind a proxy, set SECURE_PROXY_SSL_HEADER only if the proxy strips and sets the header itself |
SECURE_HSTS_SECONDS | Start small (for example 3600), then raise to a year or more | security.W004 | Django warns that careless HSTS can cause serious, irreversible problems |
SECURE_HSTS_INCLUDE_SUBDOMAINS, SECURE_HSTS_PRELOAD | True once every subdomain is HTTPS-only | security.W005, W021 | Preload is hard to undo; treat it as a one-way decision |
SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURE | True | security.W012, W016 | Stops cookies leaking over an initial HTTP request |
X_FRAME_OPTIONS | 'DENY' unless framing is needed | security.W019 | Use per-view decorators for the few pages that must be framed |
SECURE_REFERRER_POLICY, SECURE_CROSS_ORIGIN_OPENER_POLICY | Defaults same-origin | security.W022, E023, E024 | Invalid values are errors, not warnings |
SECURE_CSP | Dictionary policy, nonce-based | security.E026, W027 | W027 fires when CSP.NONCE is used without the csp context processor |
The checklist also says to replace runserver with a production WSGI or ASGI server, to restrict cache servers and databases to the application hosts, to keep Python code outside the web server's document root, and to provide custom 404.html, 500.html, 403.html and 400.html templates.
ORM and query safety
Django querysets separate SQL from parameters and let the database driver handle escaping. The risk comes from the escape hatches and from passing user-controlled structure, not just values, into the ORM.
| Pattern | Risk | Safe alternative |
|---|---|---|
Person.objects.raw("... WHERE last_name = '%s'" % name) | SQL injection through string formatting | raw("... WHERE last_name = %s", [name]) with an unquoted placeholder |
cursor.execute(f"UPDATE ... WHERE id = {x}") | SQL injection | cursor.execute("UPDATE ... WHERE id = %s", [x]) |
queryset.extra(where=[user_input]), RawSQL(user_input) | Raw fragments in otherwise safe queries | Query expressions (F, Q, Func), or parameters passed separately |
Model.objects.filter(**request.GET.dict()) | Attacker chooses lookups, relations and internal arguments; CVE-2025-64459 allowed SQL injection through a crafted _connector key in filter(), exclude(), get() and Q() | Map an allow-list of query parameters to fixed lookups, or use a filtering library with declared fields |
order_by(request.GET["sort"]) | Sorting by unintended fields, including related sensitive fields | Allow-list of sort keys mapped to field names |
Model.objects.get(pk=pk) without ownership check | Insecure direct object reference | get_object_or_404(Model, pk=pk, owner=request.user) or queryset scoping in get_queryset() |
The Django project rated CVE-2025-64459 high severity and fixed it in 5.2.8, 5.1.14 and 4.2.26. The lesson outlasts the patch: dictionary expansion lets the request decide the shape of a query. See the SQL Injection Prevention Guide.
Forms, serializers and mass assignment
Django's ModelForm documentation strongly recommends setting fields explicitly and describes fields = "__all__" and exclude as much less secure, because a field added to the model later becomes editable without anyone reviewing the form. It cites the 2012 GitHub public key incident as an example of this class of flaw. Apply the same rule to Django REST framework serializers and to any view that calls Model(**data) or update(**data). Validation in clean() methods and validators is the place for input rules; see the Input Validation and Output Encoding Guide.
Templates and XSS
- Quote every attribute: the Django security docs show that
<style class={{ var }}>can be abused even with autoescaping, whileclass="{{ var }}"cannot. - Treat safe markers as sinks: review every
|safe,mark_safe(),format_htmlmisuse and{% autoescape off %}. Preferformat_html(), which escapes its arguments, over building HTML with string concatenation andmark_safe. - Stored HTML: if users may submit rich text, sanitise it with an allow-list sanitiser on input and output, as the Django docs advise.
- JavaScript contexts: pass data to scripts with the
json_scriptfilter rather than rendering variables inside<script>blocks, because HTML escaping is the wrong encoding for JavaScript.
See the Cross-Site Scripting (XSS) Prevention Guide.
Sessions, authentication and CSRF
- Cookie defaults:
SESSION_COOKIE_HTTPONLYdefaults toTrue,SESSION_COOKIE_SAMESITEto'Lax'andSESSION_COOKIE_AGEto 1,209,600 seconds (two weeks). Shorten the age for sensitive applications and setSESSION_EXPIRE_AT_BROWSER_CLOSEwhere appropriate. - Session serialiser: the default is
JSONSerializer. Do not switch to a pickle-based serialiser; with thesigned_cookiesbackend a leakedSECRET_KEYwould then become code execution. See the Secure Deserialisation Guide. - Session fixation: use
django.contrib.auth.login(), which rotates the session key on login; custom login code that setsrequest.session["user_id"]directly does not. See the Session Fixation Guide. - Password hashing: the first entry in
PASSWORD_HASHERSis used for new hashes; Django upgrades existing hashes on login. KeepAUTH_PASSWORD_VALIDATORSenabled. - CSRF: keep
CsrfViewMiddleware, avoid@csrf_exempton cookie-authenticated views, list cross-origin form posts inCSRF_TRUSTED_ORIGINSwith full scheme, and remember that a subdomain controlled by someone else can set cookies for yours. See the CSRF Prevention Guide. - Admin:
django.contrib.adminis a high-value target. Restrict it by network or SSO at the proxy, require MFA for staff accounts, and keepis_superuserto a small group.
Secrets and the SECRET_KEY
The deployment checklist says the production key must be large, random, unique to production and never committed. Load it with os.environ["SECRET_KEY"] or from a file readable only by the application user, and treat database passwords in DATABASES the same way. To rotate, move the current key into SECRET_KEY_FALLBACKS, deploy a new SECRET_KEY, and remove the old key from the fallbacks once sessions and reset tokens issued under it have expired. A key that has leaked should be rotated without a fallback, accepting that all users are signed out.
The Django application is a non-human identity in its own right: its database role, cache credentials, email provider key and cloud storage credentials should each be scoped to the service, owned and rotated. Exposed source trees are a frequent leak path; the misconfigured Git servers study found deployment credentials in hundreds of thousands of publicly served .git/config files, which is one reason the Django checklist asks you to keep code outside the web root. See the Secrets Management Guide.
File uploads, media and CSP
- Size limits at the edge: Django advises limiting request body size in the web server rather than relying on
DATA_UPLOAD_MAX_MEMORY_SIZEorFILE_UPLOAD_MAX_MEMORY_SIZE; file uploads are not limited by the former, and under ASGI the whole request may be spooled to disk before Django validates size. - Polyglot files: the docs warn that a file with a valid PNG header followed by HTML passes
ImageFieldvalidation with Pillow and may be rendered as HTML. ServeMEDIA_URLfrom a separate registrable domain (a subdomain is not enough), allow-list extensions and setContent-Dispositionfor downloads. - No execution: make sure handlers such as PHP are never active for the media directory. See the Secure File Upload Handling Guide.
- CSP: Django 6.0 added
ContentSecurityPolicyMiddleware, theSECURE_CSPandSECURE_CSP_REPORT_ONLYsettings and nonce support through thedjango.template.context_processors.cspcontext processor. Earlier versions need a third-party package. Start withSECURE_CSP_REPORT_ONLY, then enforce. See the Content Security Policy Guide.
How to implement: a phased rollout
- Inventory (week 1): list Django versions per service against the supported versions on djangoproject.com. When this guide was written 5.2 was the LTS series with extended support until April 2028, 6.0 until April 2027 and 6.1 until December 2027.
- Deploy checks (week 1): run
manage.py check --deploywith production settings in CI, first as a report, and fix W018, W020, W009 and W001 immediately. - Secrets (weeks 1 to 3): remove keys and passwords from settings files and Git history, load them at runtime and rotate anything that was exposed.
- Transport and cookies (weeks 2 to 4): enable SSL redirect, secure cookies and a short
SECURE_HSTS_SECONDSin staging, then production; raise HSTS gradually. Roll back by lowering the value before raising it further, since browsers cache HSTS. - Code patterns (weeks 3 to 8): add Bandit or Semgrep rules for
raw,extra,RawSQL,mark_safe,csrf_exempt,fields = "__all__"and**request.GET, triage, then make new findings blocking. - CSP and gating (ongoing): report-only CSP for a release cycle, then enforce; make
check --deploy --fail-level WARNINGa merge gate once the backlog is clear.
A small team with one Django service can do steps 1 to 4 in a sprint. Larger estates benefit from a shared settings base module and a cookiecutter or template so new services start compliant.
Detection and measurement
- Log signals:
DisallowedHosterrors fromdjango.security.DisallowedHost(Host header probing), CSRF verification failures,SuspiciousOperationevents, spikes in failed logins and database errors containing quote characters. - Error reporting: route
django.securityloggers to your SIEM rather than to email, and scrub sensitive POST data with@sensitive_post_parametersand@sensitive_variables. - Metrics: services passing
check --deploywith zero warnings; services on a Django series with security support; median days from a Django security release to production; open raw SQL andmark_safefindings; ModelForms using__all__orexclude; services with an enforced CSP.
Framework mapping
| Django control area | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | OWASP ASVS 5.0 | MITRE ATT&CK addressed |
|---|---|---|---|---|
| ORM safety, explicit form fields | SI-10 | PR.PS-06 | V1.2.4, V15.3.3 | T1190 Exploit Public-Facing Application |
| Sessions and cookies | SC-23 | PR.AA-05 | V3.3.1 | T1539 Steal Web Session Cookie |
| HTTPS, HSTS, headers | SC-8 | PR.DS-02 | V3.4.1, V3.4.4 | |
| SECRET_KEY and credentials | SC-12, SC-28 | PR.DS-01 | V13.3.1 | T1552.001 Credentials In Files |
| DEBUG off, deploy checks | SA-15, SA-11 | PR.PS-06 | V13.4.2 | |
| Supported versions and patching | RA-5, SR-3 | ID.RA-01 | V15.2.1 | T1195.002 Compromise Software Supply Chain |
In CIS Controls v8.1 this sits in Control 16 (Application Software Security); in ISO/IEC 27001:2022 Annex A in 8.25 (secure development life cycle), 8.28 (secure coding) and 8.9 (configuration management).
Common failure patterns
- DEBUG in production: a debug error page reveals settings, paths and local variables, often including connection strings.
- Wildcard hosts:
ALLOWED_HOSTS = ["*"]added to silence a load balancer health check enables poisoned password reset links. - Development key in production: the generated
django-insecure-key, or a key shared across environments, ships to production and can be found in the repository. - Proxy header trust:
SECURE_PROXY_SSL_HEADERset while the proxy passes client-suppliedX-Forwarded-Protothrough, which Django warns can create CSRF weaknesses. - Request-driven queries:
filter(**request.GET.dict())and user-controlledorder_by, the pattern behind CVE-2025-64459. - Media on the main origin: user uploads served from the application domain, turning a polyglot upload into stored XSS.
Practitioner checklist
- Run
manage.py check --deploywith production settings in CI and resolve every warning or document why it does not apply. - Set
DEBUG = False, explicitALLOWED_HOSTS, secure cookies, SSL redirect and staged HSTS. - Load
SECRET_KEYand database credentials at runtime, never commit them, and rotate usingSECRET_KEY_FALLBACKS. - Ban string-formatted SQL,
extra()with user input and**requestdictionary expansion into querysets. - List
fieldsexplicitly on every ModelForm and serializer. - Scope object lookups to the requesting user or tenant in
get_queryset(). - Add login and password reset throttling, and protect the admin with network restrictions and MFA.
- Serve user media from a separate domain and cap request sizes at the web server.
- Track Django security releases and patch within a defined window on a supported series.
Standards and references
- Django: Security in Django
- Django: Deployment checklist
- Django: Settings reference
- Django: Security releases issued: 5.2.8, 5.1.14 and 4.2.26
- Django: Download and supported versions
- OWASP: Django Security Cheat Sheet
- OWASP: Application Security Verification Standard 5.0
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: Python Security Guide · Flask Security Guide · Ruby on Rails Security Guide · Secure Code Review Guide