Django SQL Injection via QuerySet.raw()

CRITICAL

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

Rule Information

Language
Python
Category
Django
Author
Shivasurya
Shivasurya
Last Updated
2026-03-22
Tags
pythondjangosql-injectionraw-querysetormtaint-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-002 --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 argument of Model.objects.raw() without proper parameterization.

Django's Model.objects.raw() provides a way to execute raw SQL queries while returning model instances. It accepts an optional params argument for safe parameterization, similar to cursor.execute(). When developers bypass this by embedding request parameters directly into the SQL string through f-strings or concatenation, attackers can inject arbitrary SQL. Since raw() operates at the ORM level, developers often have a false sense of security, not realizing that the SQL string is passed directly to the database driver without any escaping.

The rule uses inter-procedural taint analysis to follow data from Django request accessors through helper functions to the raw() call across file and module boundaries.

Security Implications

Potential attack scenarios if this vulnerability is exploited

1

ORM-Level SQL Injection

Developers using raw() sometimes believe the ORM provides automatic protection. It does not for the SQL string argument. An attacker can inject UNION SELECT clauses to return rows from arbitrary tables, bypassing any model-level access control the application enforces.

2

Mass Data Exfiltration

Because raw() returns model instances, injected UNION queries that match the expected column structure can make the application silently serve attacker-chosen data. This is particularly dangerous in APIs where the full response is serialized and returned to the caller.

3

Authentication and Authorization Bypass

If raw() is used in authentication-related queries, injection can bypass login checks or privilege verification, granting attackers access to admin functionality or other users' accounts without valid credentials.

4

Schema Discovery via Information Schema

Attackers can query information_schema tables through injection to enumerate all tables, columns, and relationships in the database, providing a blueprint for further targeted attacks against sensitive data.

How to Fix

Recommended remediation steps

  • 1Always pass user-controlled values via the params argument of raw() as a list or dict, never by embedding them in the SQL string itself.
  • 2Prefer standard Django ORM queryset methods such as filter(), exclude(), and annotate() over raw() to eliminate this vulnerability class entirely.
  • 3When raw() is required for complex queries, keep the SQL template as a static string literal and never construct it dynamically from user input.
  • 4Validate the type and format of parameters before passing them to raw(), particularly enforcing integer types for ID fields using int() conversion.
  • 5Review all occurrences of .raw() in the codebase during security reviews to ensure every call uses the params argument.

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("*.raw"). The second argument (params list) is explicitly not tracked, avoiding false positives on correctly parameterized raw() calls. Sanitizers include int(), float(), and explicit allowlist validation functions. The rule follows taint across file boundaries and through intermediate variable assignments and function call chains.

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
ISO 27001
A.14.2.5 - Secure system engineering principles

References

External resources and documentation

Similar Rules

Explore related security rules for Python

Frequently Asked Questions

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

Neither is inherently unsafe. Both raw() and cursor.execute() support safe parameterization. The difference is that raw() returns model instances while cursor.execute() returns raw rows. Both are vulnerable when user input is embedded directly in the SQL string. Both are safe when user input is passed as a separate params argument.
No. The rule uses .tracks(0) to monitor only the first argument (the SQL string). A call like Model.objects.raw("SELECT * FROM t WHERE id = %s", [user_id]) passes the tainted value as the second argument (params list), so it will not be flagged. Only calls where the tainted value is embedded into the SQL string itself trigger a finding.
SEC-001 targets the lower-level cursor.execute() path which operates on raw rows and requires database connection management. SEC-002 targets the ORM-level raw() path which returns Django model instances. Codebases using raw() often have a mistaken belief that ORM usage implies safety, making SEC-002 findings particularly important to highlight and remediate.
For ORDER BY direction and column names, use an allowlist approach since these cannot be parameterized in most databases. Validate the column name against a hardcoded set of allowed column names before including it in the SQL string. For LIMIT and OFFSET values, cast to int() to ensure they are numeric. Never pass raw user input for these structural parts of the query.
Check whether the package's raw() calls use the params argument. If a package passes user-controlled data directly into the SQL string, report it as a security vulnerability to the package maintainer. In the meantime, consider whether you can avoid passing user-controlled data to that specific package API, or whether a safer alternative package exists.
Yes. Django REST Framework views ultimately access Django's request object. Whether the source is request.GET, request.data, or request.query_params, the rule traces the taint chain to the raw() call. DRF's serializer validation does not automatically prevent injection into raw SQL -- you must still use the params argument.
Run: pathfinder scan --ruleset python/django/PYTHON-DJANGO-SEC-002 --project . This emits SARIF output suitable for GitHub Advanced Security, GitLab SAST, or any SARIF-compatible platform. Running it as a required check on pull requests prevents new raw() injection patterns from merging into the main branch.

New feature

Get these findings posted directly on your GitHub pull requests

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

See how it works