Django SQL Injection via cursor.execute()

CRITICAL

User input flows to cursor.execute() without parameterization, enabling SQL injection attacks.

Rule Information

Language
Python
Category
Django
Author
Shivasurya
Shivasurya
Last Updated
2026-03-22
Tags
pythondjangosql-injectioncursor-executedatabaseraw-sqltaint-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-001 --project .
1
2
3
4
5
6
7
8
9
10
11
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
49
50

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 raw SQL queries passed to cursor.execute() without proper parameterization.

Django's ORM provides safe, parameterized query construction by default, but developers sometimes bypass this protection by dropping down to raw SQL using Django's database connection cursor. When request.GET, request.POST, request.body, or other user-controlled data is concatenated or f-stringed into a SQL string before being passed to cursor.execute(), an attacker can inject arbitrary SQL, read or modify all database records, bypass authentication, or escalate to OS-level commands on vulnerable database servers.

The rule uses inter-procedural taint analysis to follow data across function calls and file boundaries, catching patterns like query strings built in one function and executed in another.

Security Implications

Potential attack scenarios if this vulnerability is exploited

1

Full Database Compromise

An attacker who controls the SQL string passed to cursor.execute() can use UNION SELECT to read from any table in the database, including user credentials, session tokens, and private records. There is no row-level restriction once the query is fully attacker-controlled.

2

Authentication Bypass

Login queries built with string concatenation can be bypassed with the classic ' OR '1'='1 payload. The query returns all rows, the application logs in as the first user found, which is typically an admin account.

3

Data Manipulation and Destruction

Stacked queries or subquery injection can issue INSERT, UPDATE, or DELETE statements through the same cursor.execute() call, allowing attackers to alter financial records, create backdoor accounts, or destroy application data.

4

Database Server Escalation

On PostgreSQL, COPY TO/FROM can read or write server filesystem files. On MySQL, INTO OUTFILE and LOAD DATA INFILE enable similar file access. These paths can escalate from SQL injection to full server compromise.

How to Fix

Recommended remediation steps

  • 1Always pass user input as the second argument to cursor.execute() as a list or tuple, never by concatenating it into the SQL string.
  • 2Prefer Django ORM methods (filter(), exclude(), annotate()) over raw SQL to benefit from automatic parameterization.
  • 3Validate and restrict the format of input used in SQL queries (e.g., enforce integer type for ID parameters) before it reaches the database layer.
  • 4Grant database accounts the minimum required privileges so that a compromised query cannot DROP tables or access unrelated schemas.
  • 5Enable Django's database query logging in development to audit all SQL statements and catch unparameterized queries early.

Detection Scope

How Code Pathfinder analyzes your code for this vulnerability

This rule performs inter-procedural taint analysis with global scope. Sources are Django HTTP request accessor calls: 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 (position 0, tracked via .tracks(0)) of calls("*.execute") matching Django database cursor objects. Flows that pass through django.utils.html.escape() or explicit type conversion to int/float are treated as sanitized. The rule follows tainted values through variable assignments, function arguments, and return values across 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
GDPR Article 32
Technical measures to ensure data security against unauthorized access

References

External resources and documentation

Similar Rules

Explore related security rules for Python

Frequently Asked Questions

Common questions about Django SQL Injection via cursor.execute()

The rule tracks whether user-controlled data reaches the SQL string argument (position 0) of cursor.execute(). If your code passes user input as the second argument (the parameters list), it will not be flagged. If you are still seeing a finding, check whether the tainted value is being embedded inside the SQL string itself even partially, for example via f-strings or % formatting before the execute call.
Yes. The rule uses inter-procedural analysis with global scope, meaning it follows tainted data from request.GET.get() in a view through helper functions in services.py or utils.py all the way to cursor.execute() in a separate database module. This catches the common pattern where views are thin and database logic lives in a separate layer.
Yes, Django ORM methods like filter(), exclude(), get(), and annotate() are parameterized by design and will not trigger this rule. This rule specifically targets the raw SQL path through cursor.execute(). If you replace all raw SQL usage with ORM calls, this class of vulnerability is eliminated.
Keep the SQL template string as a static literal and pass all user-controlled values in the second argument: cursor.execute("SELECT * FROM t WHERE id = %s", [user_id]). Django's database backend handles escaping for you. Never build the SQL string with f-strings or % formatting using untrusted input, even for parts of the query like column names or ORDER BY direction -- use allowlists for those.
PCI DSS v4.0 Requirement 6.2.4 explicitly mandates protection against injection attacks for any system that handles cardholder data. OWASP Top 10 A03:2021 covers Injection. NIST SP 800-53 SI-10 requires input validation. SOC 2 Type II CC6.1 covers protection of data from unauthorized access. All of these are satisfied by using parameterized queries.
Only if user-controlled input flows into the SQL string argument. Django admin and well-maintained third-party packages typically use the ORM or parameterized raw queries, so they will not be flagged. If a third-party package is flagged, that is worth investigating as a genuine vulnerability in that dependency.
If the value reaching cursor.execute() is validated (e.g., an integer ID parsed from a URL path parameter with int()), add explicit type conversion before the call. Code Pathfinder recognizes int() and float() conversions as sanitizers for SQL injection. Alternatively, use an allowlist check before the query.

New feature

Get these findings posted directly on your GitHub pull requests

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

See how it works