Django SQL Injection via QuerySet.extra()

HIGH

User input flows to QuerySet.extra() without parameterization, enabling SQL injection through Django's legacy ORM extension interface.

Rule Information

Language
Python
Category
Django
Author
Shivasurya
Shivasurya
Last Updated
2026-03-22
Tags
pythondjangosql-injectionqueryset-extraormtaint-analysisinter-proceduralCWE-89OWASP-A03
CWE References

Interactive Playground

Experiment with the vulnerable code and security rule below. Edit the code to see how the rule detects different vulnerability patterns.

pathfinder scan --ruleset python/PYTHON-DJANGO-SEC-003 --project .
1
2
3
4
5
6
7
8
9
rule.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

About This Rule

Understanding the vulnerability and how it is detected

This rule detects SQL injection vulnerabilities in Django applications where untrusted user input from HTTP request parameters flows into the SQL string arguments of QuerySet.extra() without proper parameterization.

Django's QuerySet.extra() is a legacy method that allows injecting raw SQL fragments into queryset generation. It accepts params arguments for safe parameterization of the where, select, and tables arguments. When developers embed request parameters directly into these SQL strings, attackers can manipulate the generated SQL query. The extra() method is particularly risky because its SQL fragments are injected into different parts of the generated query (WHERE, SELECT, FROM clauses), giving attackers multiple injection points.

Django's own documentation marks extra() as a last resort and recommends migrating to annotate(), filter(), and RawSQL expressions. This rule helps identify both injection vulnerabilities and opportunities to modernize the codebase.

Security Implications

Potential attack scenarios if this vulnerability is exploited

1

Multi-Point SQL Injection

Unlike cursor.execute() which has a single SQL string, extra() has multiple injection points: where, select, tables, and order_by. Each argument that accepts user input without parameterization is a separate injection vector. An attacker who controls any of these can manipulate the generated query.

2

WHERE Clause Injection

Injecting into the where argument allows attackers to append OR conditions to bypass filtering, add subqueries to exfiltrate data from other tables, or use UNION-based attacks to return arbitrary database content disguised as model data.

3

SELECT Injection and Data Leakage

The select argument adds extra columns to the queryset. An attacker who controls this argument can make Django return sensitive columns from other tables or subquery results embedded in the queryset response.

4

FROM Clause Injection via Tables Parameter

The tables argument adds extra tables to the FROM clause. An attacker can use this to perform cross-join attacks, access tables outside the application's normal data scope, or exploit implicit joins to exfiltrate data.

How to Fix

Recommended remediation steps

  • 1Migrate extra() usages to modern ORM equivalents: filter() for WHERE conditions, annotate() for computed columns, and select_related() or prefetch_related() for joins.
  • 2When extra() must be used, always pass user input through the params argument rather than embedding it in the SQL string arguments.
  • 3Treat the where, select, and tables arguments of extra() as SQL templates that must never contain user-controlled content directly.
  • 4Use Django's annotate() with RawSQL expressions as a safer alternative to extra(select=...) since RawSQL explicitly supports parameterization.
  • 5Review all extra() calls during security audits as they represent legacy patterns that warrant modernization and carry heightened injection risk.

Detection Scope

How Code Pathfinder analyzes your code for this vulnerability

This rule performs inter-procedural taint analysis with global scope. Sources include calls("request.GET.get"), calls("request.POST.get"), calls("request.GET.__getitem__"), calls("request.POST.__getitem__"), calls("request.body"), and calls("request.read"). The sink is any string argument to calls("*.extra") that contains tainted data (tracked via .tracks(0) for the primary SQL string argument). The separate params argument is not tracked. Sanitizers include int(), float(), and allowlist-based validation. The rule follows taint across module and file boundaries.

Compliance & Standards

Industry frameworks and regulations that require detection of this vulnerability

CWE Top 25
CWE-89 ranked #3 in 2023 Most Dangerous Software Weaknesses
OWASP Top 10
A03:2021 - Injection
PCI DSS v4.0
Requirement 6.2.4 - protect web-facing applications against injection attacks
NIST SP 800-53
SI-10: Information Input Validation
SANS Top 25
Insecure Interaction Between Components

References

External resources and documentation

Similar Rules

Explore related security rules for Python

Frequently Asked Questions

Common questions about Django SQL Injection via QuerySet.extra()

extra() has multiple SQL injection surfaces: the where, select, tables, and order_by arguments can all contain injected SQL. When a developer embeds user input into any of these, the injection point is in a different part of the generated query (WHERE, SELECT, or FROM clause). This multiplicity of injection points makes extra() more dangerous than single-string methods like raw() or cursor.execute().
Yes, and that is the preferred fix. Most extra(where=...) patterns can be replaced with filter() or Q() objects. extra(select=...) patterns typically migrate to annotate() with Value(), F() expressions, or RawSQL with params. The Django documentation includes a migration guide from extra() to modern ORM alternatives. This migration eliminates the vulnerability and improves code maintainability.
No. When user input is passed as the params argument (not embedded in the SQL strings), the taint analysis confirms the SQL strings are static literals and will not flag the call. For example, extra(where=["col = %s"], params=[user_val]) is safe and will not be flagged.
In rare cases involving database-specific SQL features not exposed through the ORM, extra() may still be used. In these cases, ensure all user-controlled values go through the params argument. Consider whether the database-specific feature can be moved to a stored procedure or handled with RawSQL expressions instead.
Django has been providing alternatives since version 1.8 with conditional expressions and 2.0 with window functions. By Django 3.x, the ORM can handle virtually all use cases that previously required extra(). The Django documentation explicitly states that extra() may be deprecated in a future release, making migration a maintenance priority in addition to a security priority.
Use an allowlist: validate that the direction string equals 'asc' or 'desc' (case insensitive), then construct a string like '-field_name' for descending or 'field_name' for ascending and pass it to order_by(). Never pass user-controlled column names or directions directly to order_by() or extra() without allowlist validation.

New feature

Get these findings posted directly on your GitHub pull requests

The Django SQL Injection via QuerySet.extra() rule runs in CI and posts inline review comments on the exact lines — no dashboard, no SARIF viewer.

See how it works