Django Gives You Security for Free — Here's What That Actually Means

March 14, 2026, 12:26 p.m.

When I started learning Django, I kept reading that it was "batteries included" and "secure by default". I nodded along without really understanding what that meant in practice.

Then I started studying the OWASP Top 10 — a list of the ten most critical web application security risks. And I realised something: Django had already handled most of them. Silently. Before I wrote a single line of code.

Here's what Django is doing for you right now, without you asking.


1. SQL Injection — handled by the ORM

What it is: SQL injection is when an attacker inserts malicious SQL code into a user input, which then gets executed by your database. The classic example is a login form where entering ' OR '1'='1 bypasses authentication entirely.

What Django does: The ORM — Object-Relational Mapper, the layer that translates Python code into SQL queries — never inserts user input directly into a query string. It uses parameterized queries, where the query structure and the data are sent to the database separately. The database treats the input as data, never as code.

# Django does this automatically under the hood
Post.objects.filter(title=user_input)
# → SELECT * FROM post WHERE title = %s   [user_input passed separately]

When it doesn't protect you: when you write raw SQL using string formatting.

# ❌ Bypasses all ORM protection
Post.objects.raw(f"SELECT * FROM post WHERE title = '{user_input}'")

2. XSS — handled by the template engine

What it is: XSS stands for Cross-Site Scripting. It happens when user-supplied content is rendered as HTML without being sanitised first. An attacker submits a post containing <script>document.cookie</script>. Your blog renders it. Every visitor who loads that page executes the script — and the attacker gets their session cookies.

What Django does: Django's template engine escapes all variables by default. "Escaping" means converting characters that have meaning in HTML — like <, >, " — into their safe text equivalents: &lt;, &gt;, &quot;. The script tag becomes visible text instead of executable code.

{{ post.title }}
<!-- If title is "<script>alert('xss')</script>" -->
<!-- Django renders: &lt;script&gt;alert('xss')&lt;/script&gt; -->
<!-- Browser displays it as text. Nothing executes. -->

When it doesn't protect you: when you explicitly mark content as safe.

{{ post.content|safe }}

The |safe filter tells Django "trust this content, don't escape it". If that content contains user input — like markdown converted to HTML — you need to make sure it's clean before marking it safe. A markdown library handles this for you if you use one correctly. If you're converting markdown to HTML and then rendering it with |safe, which is exactly what my blog does, make sure the markdown library you're using sanitises its output.


3. CSRF — handled by middleware

What it is: Cross-Site Request Forgery. A malicious website tricks your browser into submitting a form to your app using your session credentials. Your bank transfers money. Your blog publishes posts. You never clicked anything.

What Django does: CsrfViewMiddleware generates a unique token for every session and embeds it in every form. When a form is submitted, Django checks that the token matches. A forged request from another site won't have the token, so Django rejects it with a 403.

What you have to do: add {% csrf_token %} inside every <form method="POST">. One line. Django handles the rest.


4. Password storage — handled by the auth system

What it is: storing passwords as plain text, or using weak hashing algorithms like MD5, means that a database breach exposes every user's password directly.

What Django does: Django never stores passwords in plain text. It uses PBKDF2 with SHA-256 by default — a slow hashing algorithm that makes brute-force attacks computationally expensive. Each password is also salted — a unique random value is added before hashing, so two users with the same password have different hashes.

# Django does all of this automatically
user.set_password('mypassword')
# Stored in DB: pbkdf2_sha256$600000$randomsalt$hashedvalue

You never touch the raw password. You never write the hashing logic. Django does it when you call set_password(), and verifies it when you call check_password().


5. Clickjacking — handled by middleware

What it is: an attacker embeds your site in an invisible <iframe> on their page. You think you're clicking on something harmless. You're actually clicking on your bank's "confirm transfer" button, rendered invisibly underneath.

What Django does: XFrameOptionsMiddleware adds an X-Frame-Options: DENY header to every response. This tells the browser to refuse to render the page inside a frame. The attack vector disappears.

This is enabled by default. You don't configure anything.


What Django doesn't do for you

Django handles these five well. But there are things it can't protect you from by design:

The framework secures the framework's surface. Everything outside that is yours to own.


Takeaway

Django's defaults aren't magic. They're deliberate engineering decisions made by people who understood these attacks before most of us had heard of them. The ORM was designed with parameterized queries from day one. The template engine escapes by default rather than by exception. The auth system uses slow hashing because fast hashing is a security smell.

When you use Django correctly, you get these protections for free. When you override them — |safe, @csrf_exempt, raw SQL — you're opting out of protections you probably need.

Before you reach for those overrides, make sure you know exactly what you're giving up.


Still learning — this is exactly the kind of thing I'm documenting so you don't have to figure it out the hard way.