flawopen.com/SQL Injection in Python/Django ORM

Is Django's ORM safe from SQL injection by default?

Reference page — draft, pending review
Short answer

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.

Where it's safe

UNSAFE — raw() with interpolation
User.objects.raw(
    "SELECT * FROM users WHERE "
    f"username = '{request.GET['u']}'"
)
SAFE — standard ORM API
User.objects.filter(
    username=request.GET["u"]
)
# parameterized automatically,
# regardless of what "u" contains

The two places the safety net stops

Model.objects.raw()

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.

connection.cursor() direct access

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.

Field lookups built from user-controlled strings

.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.

How to check your codebase

grep -rn "\.raw(" --include="*.py" . | grep -v "%s" grep -rn "connection.cursor()" --include="*.py" .

FAQ

Is extra() safe?

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.

Does this apply the same way to Django REST Framework?

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.

References