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 .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
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.
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.
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.
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
References
External resources and documentation
Similar Rules
Explore related security rules for Python
Django SQL Injection via cursor.execute()
User input flows to cursor.execute() without parameterization, enabling SQL injection attacks.
Django SQL Injection via QuerySet.raw()
User input flows to QuerySet.raw() without parameterization, enabling SQL injection through Django's ORM raw query interface.
Django SQL Injection via RawSQL Expression
User input flows to RawSQL() expression without parameterization, enabling SQL injection through Django's annotation system.
Frequently Asked Questions
Common questions about Django SQL Injection via QuerySet.extra()
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.