RizTech Academy logo
RizTech Academy
Authentication and PermissionsLesson 5 of 525 min

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 |safe to 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-Options middleware 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. |safe tells 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 |safe on 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

  1. Put <script>alert(1)</script> in a field and render it in a template; confirm it shows as text (auto- escaped). Then add |safe and watch it become an XSS hole — and revert.
  2. Remove {% csrf_token %} from a form and submit; confirm the 403. Reason about what @csrf_exempt would do and why you almost never want it.
  3. Set DEBUG = True and trigger an error; note how much of your app the error page reveals. Set it back to False (with ALLOWED_HOSTS).
  4. Run python manage.py check --deploy and read every warning; for each, name the risk it is flagging.
  5. Move SECRET_KEY to os.environ[...] and load it from a .env; confirm the app runs and the secret is not in the committed code.
  6. Confirm a user's stored password is a pbkdf2_sha256$… hash, not the plaintext you set.

Official documentation

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