flawopen.com/SQL Injection in Python/Django ORM
Yes, for the query API you use 99% of the time — .filter(), .get(), .exclude(), and friends all parameterize automatically. The risk is entirely concentrated in two specific escape hatches, and it's worth knowing exactly where they are.
User.objects.raw(
"SELECT * FROM users WHERE "
f"username = '{request.GET['u']}'"
)
User.objects.filter(
username=request.GET["u"]
)
# parameterized automatically,
# regardless of what "u" contains
Executes exactly the SQL string you give it. It's safe if you pass placeholders and a params list — User.objects.raw("... WHERE username = %s", [u]) — and unsafe the moment you build that string with an f-string or + instead.
Dropping to django.db.connection.cursor() for a query outside the ORM entirely bypasses Django's query building altogether — the same cursor.execute(query, params) discipline from raw DB-API code applies here, with no ORM safety net at all.
.filter(**{user_controlled_field: value}) is safe from SQL injection (Django validates the field name against the model), but it's a different risk — it can let an attacker query fields your UI never intended to expose. Worth an allow-list even though it's not the SQL-injection class of bug.
grep -rn "\.raw(" --include="*.py" . | grep -v "%s"
grep -rn "connection.cursor()" --include="*.py" .
QuerySet.extra() is deprecated precisely because of this risk — its where/select arguments accept raw SQL fragments. Avoid it; use raw() with parameters, or better, the standard query API, instead.
Yes — DRF sits on top of the same ORM, so the same raw()/cursor rules apply regardless of whether the query originates from a view, a serializer, or a viewset.