Django’s security defaults and what you must not disable
Django is secure by default — it ships with protection against the most common web vulnerabilities turned on, so you have to actively disable them to be insecure. That is the right design, and it means most Django security is about not breaking what is already there, plus flipping a handful of settings for production. This lesson covers the protections you are getting for free, the ones people disable and regret, and the production checklist that Django itself will run for you.
What Django protects you from, out of the box
The big web vulnerabilities, and how Django handles each by default:
- SQL injection — the ORM parameterises every query, so user input can never become SQL. (The only way back in is hand-written raw SQL with string formatting — which the raw-SQL lesson told you never to do.)
- Cross-site scripting (XSS) — the template engine auto-escapes all variables, so
{{ user_input }}containing<script>renders as harmless text, not executable code. You would have to explicitly mark something|safeto bypass this. - Cross-site request forgery (CSRF) — the CSRF middleware requires a valid token on every POST, so
another site cannot forge a state-changing request as your logged-in user. This is the
{% csrf_token %}you put in every form. - Clickjacking — the
X-Frame-Optionsmiddleware stops your pages being embedded in a hostile<iframe>. - Password storage — passwords are hashed with a strong, salted algorithm (PBKDF2 by default — verified,
stored as
pbkdf2_sha256$…), never stored or compared in plaintext.
All of this is on the moment you start a project. Your job is mostly to not turn it off.
The things people disable and regret
Each protection has an escape hatch, and each escape hatch is a footgun. The ones that cause real incidents:
{{ value|safe }}/mark_safe()on user input.|safetells the template not to escape a value. Using it on anything a user supplied re-opens XSS — a comment containing<script>now runs. Only ever mark your own trusted HTML safe, never user input. If you find yourself reaching for|safeon user data, stop.@csrf_exempt. This removes CSRF protection from a view. It is almost never justified for a normal form (the token is not a burden), and removing it invites forged requests. The rare legitimate case is a webhook endpoint authenticated another way — and even then, think hard.- Disabling escaping wholesale with
{% autoescape off %}over a block containing user data — same XSS hole, wider.
The pattern: the defaults are protecting you, and every "just turn this off to make it work" is usually a sign you are solving the wrong problem. The friction is the point.
The production settings that are NOT on by default
A few security settings are off in development (because they would break local work) and must be on in production. These are the ones you configure at deploy time:
DEBUG = False # never True in production — leaks code, settings, secrets
ALLOWED_HOSTS = ["nidaan.example.com"] # the domains you serve; required with DEBUG=False
SECURE_SSL_REDIRECT = True # force HTTPS
SESSION_COOKIE_SECURE = True # send the session cookie only over HTTPS
CSRF_COOKIE_SECURE = True # same for the CSRF cookie
SECURE_HSTS_SECONDS = 31536000 # tell browsers to always use HTTPS for your domain
The most important, by a wide margin, is DEBUG = False. With DEBUG = True in production, an error
page displays your source code, settings, and often environment variables — a complete map of your
application handed to any attacker who triggers an exception. DEBUG = True in production is the single most
dangerous misconfiguration in Django, and it is depressingly common. The deployment module returns to these;
the point now is that they exist and are your responsibility.
SECRET_KEY and secrets do not belong in code
SECRET_KEY signs sessions, password-reset tokens and CSRF tokens; if it leaks, those signatures can be
forged. It — and the database password, API keys, everything secret — must come from the environment,
never be committed to the repository:
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # from the environment, not the code
A secret committed to git is compromised the moment it is pushed, even to a private repo (it lives in
history forever, and repos get shared, forked, leaked). The .gitignore you set up in the setup lesson
(ignoring .env) exists for exactly this. The settings-and-secrets lesson in the best-practices module
covers the mechanics; the rule to hold now: secrets come from the environment, and the generated
django-insecure-… key is for development only.
Let Django check for you: manage.py check --deploy
You do not have to remember this list — Django will audit it. Before deploying, run:
python manage.py check --deploy
This runs Django's deployment checklist: it inspects your settings and warns about every security
setting that is unsafe for production — DEBUG on, missing SECURE_SSL_REDIRECT, insecure cookies, a weak
SECRET_KEY, and more. Treat its warnings as a pre-flight checklist and clear them before going live. It is
the easiest, highest-value security habit in the whole course: one command that catches the mistakes that
cause real breaches. Run it as part of your deploy process, every time.
Check your work
What Django protects by default. SQL injection (ORM parameterises), XSS (template auto-escaping), CSRF
(token on every POST), clickjacking (X-Frame-Options), and password hashing (PBKDF2, verified) — all on
from the start; your job is not to break them.
The escape hatches to avoid. |safe/mark_safe() on user input (re-opens XSS), @csrf_exempt
(forged requests), and wholesale {% autoescape off %} — each disables a protection; reaching for them on
user data signals a wrong approach.
Production settings not on by default. DEBUG=False (most important — True leaks everything),
ALLOWED_HOSTS, SECURE_SSL_REDIRECT, secure session/CSRF cookies, HSTS.
Secrets. SECRET_KEY and all secrets come from the environment, never committed; a committed secret is
compromised and lives in history forever. The django-insecure-… key is development-only.
The one command. manage.py check --deploy audits your settings against Django's deployment checklist
and warns about every unsafe one — run it before every deploy.
Practice
- Put
<script>alert(1)</script>in a field and render it in a template; confirm it shows as text (auto- escaped). Then add|safeand watch it become an XSS hole — and revert. - Remove
{% csrf_token %}from a form and submit; confirm the 403. Reason about what@csrf_exemptwould do and why you almost never want it. - Set
DEBUG = Trueand trigger an error; note how much of your app the error page reveals. Set it back toFalse(withALLOWED_HOSTS). - Run
python manage.py check --deployand read every warning; for each, name the risk it is flagging. - Move
SECRET_KEYtoos.environ[...]and load it from a.env; confirm the app runs and the secret is not in the committed code. - Confirm a user's stored password is a
pbkdf2_sha256$…hash, not the plaintext you set.
Official documentation
- Django — Security in Django — Every built-in protection.
- Django — Deployment checklist — What
check --deployenforces, setting by setting. - Django —
check --deploy— The command itself.
Next: Django REST Framework — serializers and views.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship