Django Security

A Practical Guide to Preventing SQL Injection in Django

SQL injection is still the most common way Django apps get breached in 2026 — almost always through .raw(), .extra(), or a stray f-string, never through the ORM itself.

F Fora AI Team July 14, 2026 7 min read

SQL injection has been on the OWASP Top 10 for over two decades, and it still shows up in production Django applications on a regular basis. That's not because Django's ORM is unsafe — it's because developers step outside the ORM to write raw SQL, and that's where things go wrong.

Where it actually happens

The Django ORM parameterizes queries by default. User.objects.filter(username=username) is safe no matter what username contains, because Django never builds a SQL string by concatenating your input — it passes the value separately to the database driver. The driver treats it strictly as data, never as part of the query structure.

The trouble starts the moment someone reaches for .raw(), .extra(), or a direct cursor.execute() call, usually to solve a performance problem or write a query the ORM can't express cleanly. That's a completely reasonable thing to need. The mistake is building the SQL string with Python string formatting instead of passing parameters:

# Vulnerable
query = "SELECT * FROM orders WHERE customer_id = " + customer_id
cursor.execute(query)

# Also vulnerable - f-strings don't make this safe
cursor.execute(f"SELECT * FROM orders WHERE customer_id = {customer_id}")

Both of these let an attacker submit something like 1 OR 1=1 or 1; DROP TABLE orders;-- as customer_id and have it interpreted as SQL rather than data.

The fix is almost always the same one line

# Safe - the driver escapes the parameter for you
cursor.execute("SELECT * FROM orders WHERE customer_id = %s", [customer_id])

Every major Python database driver supports this parameter-substitution syntax. It's not slower, it's not more verbose in any meaningful way, and it closes the vulnerability completely because the database never sees your input as part of the query text.

A subtlety: .extra() is deprecated for a reason

Django's QuerySet.extra() method has been soft-deprecated in favor of expressions and RawSQL partly because it's so easy to misuse. If you're maintaining an older codebase and see .extra(where=[...]) with string formatting inside it, treat it as a priority to audit — it has the exact same injection surface as a raw cursor call.

What actually catches this in review

The pattern is mechanical enough that it doesn't require deep security expertise to spot: any place where a SQL string is built with +, %, .format(), or an f-string, and any part of that string originates from a request (GET, POST, headers, cookies, or even another database record that a user previously controlled), is worth a second look. That's exactly what Fora AI's rule engine checks for automatically on every scan, so you don't have to remember to look for it on every pull request.

Takeaway

Stay inside the ORM whenever you can. When you can't, parameterize. The discipline costs you nothing and it's the single highest-leverage habit for keeping SQL injection out of a Django codebase.

← Back to all posts