Essential security practices for Django applications. Covers CSRF, XSS, SQL injection, HTTPS, headers, authentication, and deployment hardening.
Django is secure by default in many ways, shipping with protections against common attacks — but those defaults can be disabled, misconfigured, or undermined by application code, and several critical settings must be configured correctly for production. This practical checklist walks through the security essentials every Django deployment should get right, from the settings that matter most to the habits that keep an application safe over time.
Django was built with security as a core concern and provides strong protections out of the box — against cross-site scripting, cross-site request forgery, SQL injection, and more. This is a major reason to build on it: many risks are handled for you by default. But "secure by default" is not "secure no matter what," because every default can be turned off or worked around by application code, and some critical settings are not safe by default for production. The goal of a security checklist is to ensure you keep the protections Django gives you and correctly configure the things that are your responsibility.
The single most important production setting is disabling debug mode. With debug on, Django shows detailed error pages that expose stack traces, configuration, and internal details — invaluable in development and dangerous in production, where they hand an attacker a map of your application. Debug mode must be off in production, without exception. This is one of the most common and most serious misconfigurations, precisely because debug is on by default for development convenience and must be deliberately turned off for deployment. Verifying that debug is disabled in production is the first item on any Django security checklist.
Django uses a secret key to sign sessions, tokens, and other security-sensitive data, and the security of those depends entirely on the key staying secret. It must be a strong, random value, it must never be committed to version control, and it should be loaded from the environment rather than hard-coded in settings. A leaked secret key undermines the protections that rely on it. Treating the secret key as the critical credential it is — kept out of source control, stored securely, and rotated if ever exposed — is essential, because so much of Django's security signing depends on its confidentiality.
The allowed hosts setting tells Django which host or domain names your site is allowed to serve, and it must be set to your actual domains in production. It defends against certain attacks that rely on a forged host header, and leaving it open or misconfigured weakens that protection. Setting it explicitly to the domains your application should respond to is a small configuration step with a real security purpose. It is part of the baseline production configuration that, together with disabling debug and protecting the secret key, forms the foundation of a securely deployed Django application.
All traffic should be encrypted with HTTPS, and Django offers settings to enforce it — redirecting plain HTTP to HTTPS, marking cookies as secure so they are only sent over encrypted connections, and enabling strict transport security so browsers refuse to connect over plain HTTP. Without HTTPS, data including credentials and session cookies travels in the clear, readable by anyone on the network path. Enforcing HTTPS end to end, with the related security settings enabled, is non-negotiable for any production site, and modern certificate tooling makes it free and straightforward to set up.
Django's protections against cross-site request forgery and cross-site scripting work by default — CSRF tokens guard form submissions, and template auto-escaping neutralizes injected scripts — but they can be defeated by application code that opts out. The danger arises when you disable CSRF protection on a view or mark untrusted content as safe so it renders as raw HTML. The checklist item here is to respect these defaults: keep CSRF protection enabled and never mark user-supplied content as safe without sanitizing it. The strongest protections are the ones you do not accidentally turn off, and these two defend against two of the most common web attacks.
Django's ORM parameterizes queries, so ordinary ORM use is safe against SQL injection by construction — a major built-in protection. The risk returns only when you bypass the ORM with raw SQL built by concatenating user input, where unsanitized input can become injectable. The checklist guidance is simple: use the ORM's safe query construction and never build SQL by stitching user input into a query string. As long as you stay within the ORM's parameterized queries and treat any raw SQL with great care, this entire category of attack is effectively closed for you by the framework.
Authentication is the front door, and several practices harden it. Enforce strong passwords using Django's password validators, protect against brute-force attempts with rate limiting and lockouts on repeated failures, secure session cookies, and offer or require multi-factor authentication for sensitive or administrative accounts. Django provides solid building blocks for all of this, but you must enable and tune them for your threat model rather than accepting bare defaults. Hardening authentication is high-value because it directly defends against attackers trying to gain access with stolen or guessed credentials, which is one of the most common attack methods.
A large part of your security depends on code you did not write — Django itself and your other dependencies. Known vulnerabilities in outdated packages are exactly what automated attacks look for, so keeping Django current with its security releases and updating dependencies promptly when fixes are published is essential. Scanning your dependencies for known vulnerabilities and applying updates is ongoing hygiene rather than a one-time task. The most carefully written application can still be compromised through a single neglected, vulnerable dependency, which is why staying current is a recurring item on every security checklist.
Django includes a built-in deployment check that audits your configuration for many common security issues — debug left on, an insecure secret key, missing HTTPS settings, and more — and reports what to fix. Running it as part of your deployment process catches a great deal of the low-hanging fruit automatically, before it reaches production. This tool turns much of this checklist into an automated verification, so a forgotten setting is flagged rather than silently shipped. Making the deployment check a routine step is one of the easiest, highest-value habits for keeping a Django application's configuration secure.
An important mindset for the whole checklist is that security comes from layers, not one decisive measure. No single setting makes an application secure; rather, each item closes off a class of risk, and together they form overlapping defenses so that one gap does not mean compromise. HTTPS protects data in transit, the secret key protects signed data, access controls protect resources, and dependency updates protect against known flaws — each addressing a different threat. Approaching security as building up these layers, rather than hunting for a magic bullet, is what produces an application that is genuinely hard to attack, because an attacker who gets past one protection still faces the others.
One of the most important and most application-specific protections is access control: ensuring users can only access the data and actions they are permitted to. Django's authentication identifies who a user is, but authorization — what they may do — is largely your responsibility in each view. The common flaw is fetching an object by an identifier from the URL without checking the user owns it, letting someone access another's data by changing the identifier. Always scoping data to the permitted user and checking permissions on every view that touches user-owned resources closes off this category, which is consistently among the most serious real-world web vulnerabilities.
Treating all input as potentially hostile is a foundational security habit. Validate data coming from users — through Django's forms and serializers, which provide structured validation — and be careful about how user-supplied content is used and displayed. The framework's protections handle much of this, but your validation logic enforces your application's specific rules and rejects malformed or malicious input before it causes harm. Combined with the built-in protections against injection and scripting, rigorous input validation at your application's boundaries is what keeps untrusted data from becoming a vector for attack, and it is a habit worth applying consistently to every place input enters.
You cannot respond to an attack you cannot see, so logging and monitoring security-relevant events is itself a protection. Recording authentication failures, access denials, and suspicious activity, and watching those signals, turns a silent breach into one you detect and respond to. Without monitoring, an attacker can probe and exploit your application unnoticed for a long time. Building visibility into security events from the start — and ensuring logs themselves are protected and never contain sensitive data — means that when something goes wrong, you find out quickly and have the information to understand and contain it, rather than learning about it from the damage.
The final and most important habit is recognizing that security is not a one-time checklist you complete and forget, but an ongoing practice. New vulnerabilities are discovered, dependencies need updating, new features introduce new risks, and threats evolve. Keeping Django and dependencies patched, reviewing new code for security implications, periodically re-running the deployment check, and staying aware of emerging risks are continuous activities. An application that was secure at launch drifts toward insecurity if neglected. Treating security as a sustained discipline woven into how you develop and operate, rather than a box ticked once, is what keeps an application safe over its whole life.
Django gives you strong security by default, but a secure production deployment requires keeping those protections and configuring the settings that are your responsibility. The essentials: turn debug off in production, protect the secret key and keep it out of source control, set allowed hosts to your real domains, and enforce HTTPS with secure cookies and strict transport security. Respect the built-in CSRF and XSS protections by never disabling them or marking untrusted content as safe, stay within the ORM's parameterized queries to avoid SQL injection, and harden authentication with strong passwords, rate limiting, and multi-factor for privileged accounts. Keep Django and your dependencies patched, since outdated packages are a prime target, and run Django's deployment check to catch misconfigurations automatically. Working through this checklist closes the great majority of real-world risk, because it aligns directly with how applications actually get attacked — and most of it is configuration and habit rather than complex engineering.