Preventing SQL Injection in Django
A practical security guide to avoiding SQL injection in Django: safe ORM usage, parameterizing raw queries, and avoiding extra() pitfalls.

Django is widely celebrated for its robust built-in security features. Its Object-Relational Mapper (ORM) automatically abstracts database interactions and constructs parameterized queries for standard operations like filter(), exclude(), and get(). Because of these built-in protections, many developers assume that a Django application is completely immune to SQL injection.
However, when engineering teams resort to raw SQL execution, dynamic clause building, or legacy QuerySet methods, dangerous injection vulnerabilities frequently emerge.
To understand the underlying threat models and attacker techniques, read our foundational guide on how SQL injection works and why it remains dangerous. If your stack also incorporates Node.js services, review our companion guide on preventing SQL injection in Node.js and Express.
In this tutorial, we analyze how SQL injection manifests in Django and how to defend your application.
Why Django applications remain susceptible to SQLi
The Django ORM sanitizes values passed into standard QuerySet filters:
# SECURE: Django ORM parameterizes the query automatically
users = User.objects.filter(username=request.POST.get('username'))
Here, the ORM handles escaping and parameter binding natively. SQL injection occurs in Django when developers bypass ORM query construction or misuse specific QuerySet methods.
1. Misusing RawSQL and string interpolation
When custom database logic requires raw SQL, developers often use RawSQL or Manager.raw() with Python f-strings:
# DANGEROUS: Python string interpolation creates SQLi
from django.db.models.expressions import RawSQL
user_input = request.GET.get('order_by', 'id')
# Vulnerable string formatting:
users = User.objects.annotate(
val=RawSQL(f"SELECT score FROM leaderboards WHERE category = '{user_input}'", [])
)
If user_input contains ' OR 1=1 --, the SQL logic is altered.
2. Legacy QuerySet.extra() risks
The extra() method was historically used for appending raw SQL fragments. Because it makes it trivial to inject untrusted input into select, where, or order_by clauses, the Django core team explicitly deprecated it:
# DANGEROUS: Legacy extra() with unparameterized where clause
Entry.objects.extra(where=[f"headline = '{request.GET.get('title')}'"]) # VULNERABLE
Safe raw queries with Manager.raw()
When a complex analytical query cannot be expressed using standard ORM expressions, use Model.objects.raw() with the params parameter:
# SECURE: Model.raw() with parameter binding
from myapp.models import Product
def get_products_by_category(category_name):
query = """
SELECT id, name, price, stock
FROM myapp_product
WHERE category = %s AND active = %s
"""
params = [category_name, True]
# Django parameterizes %s using the underlying database driver
return Product.objects.raw(query, params)
[!IMPORTANT] Always pass arguments as a list or tuple to
params. Never format parameters using Python%string formatting or f-strings before passing them toraw().
Executing custom SQL via database cursors
For operations that do not map directly to models (such as bulk data pipelines or reporting views), use Django's database connection cursor:
# SECURE: Using connection.cursor() with parameterized queries
from django.db import connection
def get_user_statistics(tenant_id, min_purchases):
query = """
SELECT user_id, COUNT(*) as total_orders, SUM(amount) as total_spent
FROM orders
WHERE tenant_id = %s
GROUP BY user_id
HAVING COUNT(*) >= %s
"""
with connection.cursor() as cursor:
cursor.execute(query, [tenant_id, min_purchases])
results = cursor.fetchall()
return results
In this pattern:
- The SQL string contains
%splaceholders. cursor.execute()sends the query template and values separately to the database engine.- Injection payloads are treated as harmless string literals.
Safeguarding dynamic order_by clauses
A subtle vulnerability in Django occurs when users can specify sorting columns via URL query parameters:
# DANGEROUS: Direct user input into order_by()
sort_field = request.GET.get('sort', 'created_at')
articles = Article.objects.order_by(sort_field) # Potentially exposes internal field paths
While Django's order_by() resists basic string injection, untrusted input can leak relational model structures or cause unexpected exceptions. Implement a strict whitelist:
# SECURE: Whitelisting allowed sorting fields
ALLOWED_SORT_FIELDS = {
'date': '-created_at',
'title': 'title',
'views': '-view_count',
}
sort_key = request.GET.get('sort', 'date')
order_clause = ALLOWED_SORT_FIELDS.get(sort_key, '-created_at')
articles = Article.objects.order_by(order_clause)
To establish dependable code verification habits across your engineering organization, refer to our clean code and code review habits guide.
Common Django database mistakes
| Anti-Pattern | Root Cause | Recommended Fix |
|---|---|---|
Using QuerySet.extra() |
Legacy API allowing unparameterized SQL | Refactor to standard ORM annotations or RawSQL with params |
f-strings inside RawSQL |
Bypassing parameter tuple argument | Pass query values inside the second params argument list |
| Trusting raw column identifiers | Assuming parameter binding works on column names | Whitelist column and table names using Python set or dict |
| Direct string substitution in cursor.execute | Writing cursor.execute(f"SELECT ... {var}") |
Pass variables strictly via the second parameter: cursor.execute(sql, [var]) |
| Uncaught database errors returning debug views | DEBUG = True enabled on production environments |
Ensure DEBUG = False and log database exceptions privately |
Django security review checklist
Before deploying Django applications to production, verify these security controls:
- Zero occurrences of
QuerySet.extra()exist in the codebase. - All
cursor.execute()calls supply arguments via theparamsparameter; zero f-strings or.format()calls. - User-controllable sorting parameters are validated against an explicit whitelist of allowed field names.
-
DEBUGis set toFalsein productionsettings.pyto prevent database schema exposure in tracebacks. - Database credentials operate under a role with minimum necessary table and schema permissions.
Bottom line
Django's ORM provides outstanding out-of-the-box protection against SQL injection. By eliminating deprecated methods like extra(), strictly passing arguments through params in raw SQL and cursor calls, and whitelisting dynamic query fields, your Django backend remains resilient against all forms of SQL injection attacks.
Sources: Django Documentation on Performing Raw SQL Queries, OWASP Django Security Checklist, CWE-89: SQL Injection (evaluated November 2026).


