Top 5 SQL Injection Mistakes in Django Apps (And How to Fix Them)
Why Django Apps Still Get SQL Injection
Django's ORM is one of the safest database abstraction layers available. It parameterizes queries automatically, escapes inputs, and discourages raw SQL. Yet in our code review engagements, 1 in 4 Django applications had at least one SQL injection vulnerability.
Why? Because developers bypass the ORM when they need complex queries — and that's where mistakes happen.
Fact: SQL injection remained the #1 web vulnerability for over a decade. OWASP 2025 ranks injection attacks as the third most critical risk (A03).
Mistake #1: Using .raw() with String Formatting
This is the most common SQLi we find in Django code reviews.
❌ Vulnerable Code
# DANGEROUS — user input directly in raw SQL
def search_users(request):
query = request.GET.get('q', '')
users = User.objects.raw(
f"SELECT * FROM auth_user WHERE username LIKE '%{query}%'"
)
return render(request, 'results.html', {'users': users})
The problem: f-strings and .format() insert user input directly into the SQL string. An attacker can send q=' OR 1=1-- to dump the entire user table.
✅ Secure Code
# SAFE — parameterized query
def search_users(request):
query = request.GET.get('q', '')
users = User.objects.raw(
"SELECT * FROM auth_user WHERE username LIKE %s",
[f'%{query}%']
)
return render(request, 'results.html', {'users': users})
Why it's safe: Django passes the parameter separately to the database driver, which handles escaping.
Mistake #2: Using cursor.execute() Unsafely
When developers need database features not supported by the ORM, they reach for raw cursors.
❌ Vulnerable Code
from django.db import connection
def get_report(request, report_id):
with connection.cursor() as cursor:
# DANGEROUS — string concatenation
cursor.execute(
"SELECT * FROM reports WHERE id = " + report_id
)
row = cursor.fetchone()
return JsonResponse({'report': row})
✅ Secure Code
from django.db import connection
def get_report(request, report_id):
with connection.cursor() as cursor:
# SAFE — parameterized
cursor.execute(
"SELECT * FROM reports WHERE id = %s",
[report_id]
)
row = cursor.fetchone()
return JsonResponse({'report': row})
Mistake #3: The extra() Method
Django's deprecated extra() method is a top source of injection.
❌ Vulnerable Code
# DANGEROUS — extra() with user input
def filter_products(request):
order = request.GET.get('sort', 'name')
products = Product.objects.extra(
order_by=[order] # Attacker: sort=name; DROP TABLE products--
)
return render(request, 'products.html', {'products': products})
✅ Secure Code
# SAFE — whitelist approach
ALLOWED_SORT_FIELDS = {'name', 'price', 'created_at', '-name', '-price', '-created_at'}
def filter_products(request):
order = request.GET.get('sort', 'name')
if order not in ALLOWED_SORT_FIELDS:
order = 'name'
products = Product.objects.order_by(order)
return render(request, 'products.html', {'products': products})
Mistake #4: Dynamic Table/Column Names
ORM parameters can only substitute values, not table or column names.
❌ Vulnerable Code
# DANGEROUS — dynamic column name
def flexible_search(request):
field = request.GET.get('field', 'username')
value = request.GET.get('value', '')
with connection.cursor() as cursor:
cursor.execute(
f"SELECT * FROM auth_user WHERE {field} = %s",
[value]
)
results = cursor.fetchall()
return JsonResponse({'results': results})
Attack: An attacker sets field=1=1 OR username to bypass filtering entirely.
✅ Secure Code
ALLOWED_FIELDS = {'username', 'email', 'first_name', 'last_name'}
def flexible_search(request):
field = request.GET.get('field', 'username')
value = request.GET.get('value', '')
if field not in ALLOWED_FIELDS:
return JsonResponse({'error': 'Invalid field'}, status=400)
with connection.cursor() as cursor:
cursor.execute(
f"SELECT * FROM auth_user WHERE {field} = %s",
[value]
)
results = cursor.fetchall()
return JsonResponse({'results': results})
Mistake #5: JSON/JSONB Queries with String Interpolation
With PostgreSQL JSONB fields becoming common, we see injection in JSON path queries.
❌ Vulnerable Code
# DANGEROUS — JSON path injection
def search_metadata(request):
key = request.GET.get('key', 'name')
value = request.GET.get('value', '')
with connection.cursor() as cursor:
cursor.execute(
f"SELECT * FROM products WHERE metadata->>'{key}' = '{value}'"
)
results = cursor.fetchall()
✅ Secure Code
ALLOWED_KEYS = {'name', 'category', 'brand'}
def search_metadata(request):
key = request.GET.get('key', 'name')
value = request.GET.get('value', '')
if key not in ALLOWED_KEYS:
return JsonResponse({'error': 'Invalid key'}, status=400)
with connection.cursor() as cursor:
cursor.execute(
f"SELECT * FROM products WHERE metadata->>'{key}' = %s",
[value]
)
results = cursor.fetchall()
Prevention Checklist
| Rule | Implementation |
|---|---|
| Always use ORM | User.objects.filter(username=q) instead of raw SQL |
| Parameterize everything | Pass values as the second argument to .raw() and .execute() |
| Never use f-strings in SQL | Even for "simple" queries |
| Whitelist identifiers | Table names, column names, sort orders — use allow-lists |
Remove extra() calls | Use .annotate(), .order_by(), or Subquery instead |
| Automated scanning | Add bandit to CI/CD: bandit -r . -t B608 catches SQL injection |
| Code review | Every PR with raw SQL gets a security review |
How We Can Help
At SecureCodeReviews, we perform manual security code reviews of Django applications. We've reviewed 100+ Django projects and found SQL injection in 25% of them — even in apps built by experienced teams.
Request a Free Sample Code Review → — Send us 20–30 lines of your Django code and get a free mini security report.
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
OWASP Top 10 2025: What's Changed and How to Prepare
A comprehensive breakdown of the latest OWASP Top 10 vulnerabilities and actionable steps to secure your applications against them.
The Ultimate Secure Code Review Checklist for 2025
A comprehensive, actionable checklist for conducting secure code reviews. Covers input validation, authentication, authorization, cryptography, error handling, and CI/CD integration with real-world examples.
SQL Injection Prevention: Complete Guide with Code Examples
Master SQL injection attacks and learn proven prevention techniques. Includes vulnerable code examples, parameterized queries, and real-world breach analysis.