Django SQL Injection via RawSQL Expression

CRITICAL

User input flows to RawSQL() expression without parameterization, enabling SQL injection through Django's annotation system.

Rule Information

Language
Python
Category
Django
Author
Shivasurya
Shivasurya
Last Updated
2026-03-22
Tags
pythondjangosql-injectionrawsqlannotationsormtaint-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-004 --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

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 argument of RawSQL() expressions without proper parameterization.

Django's RawSQL() expression allows embedding raw SQL fragments within ORM querysets using annotate(), filter(), and order_by(). It is designed as a safe replacement for the deprecated extra() method and explicitly supports a params argument for parameterized values. When developers embed request parameters directly into the SQL string of a RawSQL() expression instead of using the params argument, they introduce SQL injection at the ORM annotation level.

RawSQL injection is particularly insidious because the expression appears in a modern, ORM-integrated pattern, leading developers to assume ORM-level protection exists. The SQL string is passed verbatim to the database query builder without escaping.

Security Implications

Potential attack scenarios if this vulnerability is exploited

1

Annotation-Level Data Exfiltration

An attacker who controls the SQL string of a RawSQL() annotation can use subqueries to exfiltrate data from arbitrary tables, embedding the results as annotation values in the queryset response. The application may serialize these attacker-controlled annotation values back to the client.

2

WHERE Clause Manipulation via Filtered Annotations

When RawSQL() is used in filter() conditions, injection into the SQL string allows attackers to append OR conditions, bypass access controls, or read records belonging to other users by escaping the intended filter logic.

3

ORDER BY Injection

RawSQL() used in order_by() with user-controlled SQL strings enables ORDER BY injection attacks. On some databases, injection here can trigger time-based blind SQL injection using CASE WHEN and sleep-like functions, allowing data extraction character by character.

4

Stacked Query Execution

On database backends that support multiple statements (e.g., PostgreSQL with certain drivers), injection into RawSQL() may enable stacked queries, allowing attackers to execute INSERT, UPDATE, DELETE, or DDL statements alongside the intended query.

How to Fix

Recommended remediation steps

  • 1Always pass user-controlled values via the params tuple (second argument) of RawSQL(), never by embedding them in the SQL string.
  • 2Consider using Django ORM expressions (F(), Value(), Case(), When(), ExpressionWrapper()) as type-safe alternatives to RawSQL() that require no parameterization.
  • 3Validate and cast numeric parameters with int() or float() before using them in queries, even when they will be parameterized.
  • 4Treat the SQL string argument of RawSQL() as a static template constant -- if it contains runtime values, those must go in params.
  • 5Review all RawSQL() usages during code review to verify the params argument is used for any values that could be user-influenced.

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 the first argument (SQL string, tracked via .tracks(0)) of calls("RawSQL"). The second argument (params tuple) is not tracked. Sanitizers include int(), float(), and explicit allowlist validation. The analysis follows taint chains across file and module boundaries to catch multi-hop flows.

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
SOC 2 Type II
CC6.1 - Logical access controls to protect against unauthorized data access

References

External resources and documentation

Similar Rules

Explore related security rules for Python

Frequently Asked Questions

Common questions about Django SQL Injection via RawSQL Expression

Yes. RawSQL() is designed to be used safely by putting user values in the params tuple. The SQL string itself is still passed as a raw fragment to the database query builder. If you embed user input into that string via f-strings or concatenation instead of using params, you have the same SQL injection vulnerability as extra(). RawSQL() is only safe when the SQL string is a static literal.
RawSQL() operates within the ORM annotation system, meaning injection here affects annotations, filters, and ordering computed within queryset chain calls. Unlike cursor.execute(), the injected SQL is a fragment embedded inside a larger ORM-generated query, which can make extraction more complex for attackers but does not reduce the fundamental risk.
No. annotate() processes the RawSQL() expression object, which includes the SQL string verbatim in the generated query. The ORM does not inspect or escape the string. All safety guarantees come from proper use of the params argument.
Django's built-in ORM expressions (F(), Value(), Case(), When(), Coalesce()) are compiled to the same SQL as equivalent RawSQL() calls, often with identical execution plans. For complex database-specific functions not in Django's built-in set, use database functions (Func subclasses) which are also parameterizable. Profile both approaches in your specific environment if performance is a concern.
For sort direction (ASC/DESC), use Django's standard order_by() with F() and the descending() method. For user-specified column names, validate against an allowlist of permitted field names before including them in the RawSQL() SQL string -- column names cannot be parameterized in SQL. Never pass raw user input as a column name.
Run: pathfinder scan --ruleset python/django/PYTHON-DJANGO-SEC-004 for injection flows. For a full audit of all RawSQL() calls regardless of taint, use PYTHON-DJANGO-SEC-005 (raw SQL audit rule) which performs a pattern match on DjangoExpressions.method("RawSQL") without requiring taint analysis.

New feature

Get these findings posted directly on your GitHub pull requests

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

See how it works