Vulnerability Research
SQL Injection
Django
Python
OWASP

Top 5 SQL Injection Mistakes in Django Apps (And How to Fix Them)

SecureCodeReviews Team
March 10, 2026
10 min read
Share

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

RuleImplementation
Always use ORMUser.objects.filter(username=q) instead of raw SQL
Parameterize everythingPass values as the second argument to .raw() and .execute()
Never use f-strings in SQLEven for "simple" queries
Whitelist identifiersTable names, column names, sort orders — use allow-lists
Remove extra() callsUse .annotate(), .order_by(), or Subquery instead
Automated scanningAdd bandit to CI/CD: bandit -r . -t B608 catches SQL injection
Code reviewEvery 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