SQL Injection via FilteredRelation column aliases on PostgreSQL
Description
The commit implements a real security fix for CVE-2025-13372 by guarding against dollar signs in column aliases for PostgreSQL when using FilteredRelation. It adds a runtime guard in PostgreSQL's SQL compiler to prevent alias names containing '$' from being used, and augments tests/docs to reflect the security improvement. This is not merely a dependency bump or a cleanup; it changes behavior to mitigate a SQL injection surface related to alias generation in FilteredRelation on PostgreSQL.
Proof of Concept
Proof-of-concept (conceptual) to illustrate the vulnerability path before the fix:
Context and goal:
- PostgreSQL allows dollar-quoted strings and complex aliasing scenarios. In vulnerable Django builds, building a QuerySet with FilteredRelation and a crafted alias that contains a dollar sign could be incorporated into the generated SQL without proper escaping, creating a potential SQL injection vector via the alias name when dictionary expansion (**) is used in annotating or aliasing.
Prerequisites:
- Django version on the 5.1.x branch prior to 5.1.15 (pre-fix)
- PostgreSQL database
- A minimal app with models Book and Author (with a relation suitable for FilteredRelation)
PoC steps (conceptual, executable in a local test environment):
1) Define models (simplified):
class Book(models.Model):
name = models.CharField(max_length=100)
authors = models.ManyToManyField('Author')
class Author(models.Model):
name = models.CharField(max_length=100)
2) Create a QuerySet that uses a crafted alias with a dollar sign via FilteredRelation (vulnerable behavior pre-fix):
# Vulnerable (pre-fix) usage, relying on **kwargs expansion into alias labels
qs = Book.objects.alias(**{"evil_alias$": FilteredRelation("authors")})
# Accessing/values would incorporate the alias into the generated SQL
result_qs = qs.values("name", "evil_alias$")
3) Trigger execution and observe the generated SQL (via Django's debugging or connection.queries):
list(result_qs)
Expected (pre-fix) observation:
- The generated SQL includes the column alias name containing a dollar sign (evil_alias$) and is not robustly escaped in all code paths. In some crafted payload scenarios, an attacker could append or inject SQL through the alias portion, exploiting improper escaping to influence the surrounding query.
4) With the fix in place (as of this commit), attempting to use an alias containing '$' raises a ValueError:
ValueError: Dollar signs are not permitted in column aliases on PostgreSQL.
Notes:
- The tests added in tests/annotations/tests.py explicitly exercise this guard by trying an alias that contains a '$' and asserting the ValueError on PostgreSQL. This confirms the vulnerability mitigation. The PoC above is a conceptual demonstration of the surface that the fix protects against; actual exploitation would depend on the specifics of the generated SQL and database parsing behavior in the vulnerable version.
Commit Details
Author: Jacob Walls
Date: 2025-11-17 22:09 UTC
Message:
Fixed CVE-2025-13372 -- Protected FilteredRelation against SQL injection in column aliases on PostgreSQL.
Follow-up to CVE-2025-57833.
Thanks Stackered for the report, and Simon Charette and Mariusz Felisiak
for the reviews.
Triage Assessment
Vulnerability Type: SQL Injection
Confidence: HIGH
Reasoning:
Commit documents and implements a fix for CVE-2025-13372 by preventing dollar signs in column aliases for PostgreSQL, which could be used to craft SQL injection in FilteredRelation. The code adds a guard in quote_name_unless_alias and updates tests/docs to reflect the security fix.
Verification Assessment
Vulnerability Type: SQL Injection via FilteredRelation column aliases on PostgreSQL
Confidence: HIGH
Affected Versions: 5.1.x before 5.1.15 (i.e., 5.1.0 through 5.1.14)
Code Diff
diff --git a/django/db/backends/postgresql/compiler.py b/django/db/backends/postgresql/compiler.py
index 48d0ccfd9d06..08d78e333a5d 100644
--- a/django/db/backends/postgresql/compiler.py
+++ b/django/db/backends/postgresql/compiler.py
@@ -1,6 +1,6 @@
from django.db.models.sql.compiler import ( # isort:skip
SQLAggregateCompiler,
- SQLCompiler,
+ SQLCompiler as BaseSQLCompiler,
SQLDeleteCompiler,
SQLInsertCompiler as BaseSQLInsertCompiler,
SQLUpdateCompiler,
@@ -25,6 +25,15 @@ def __str__(self):
return "UNNEST(%s)" % ", ".join(self)
+class SQLCompiler(BaseSQLCompiler):
+ def quote_name_unless_alias(self, name):
+ if "$" in name:
+ raise ValueError(
+ "Dollar signs are not permitted in column aliases on PostgreSQL."
+ )
+ return super().quote_name_unless_alias(name)
+
+
class SQLInsertCompiler(BaseSQLInsertCompiler):
def assemble_as_sql(self, fields, value_rows):
# Specialize bulk-insertion of literal values through UNNEST to
diff --git a/docs/releases/4.2.27.txt b/docs/releases/4.2.27.txt
index 7ffa5fa45858..e95dc63f74ef 100644
--- a/docs/releases/4.2.27.txt
+++ b/docs/releases/4.2.27.txt
@@ -7,6 +7,14 @@ Django 4.2.27 release notes
Django 4.2.27 fixes one security issue with severity "high", one security issue
with severity "moderate", and one bug in 4.2.26.
+CVE-2025-13372: Potential SQL injection in ``FilteredRelation`` column aliases on PostgreSQL
+============================================================================================
+
+:class:`.FilteredRelation` was subject to SQL injection in column aliases,
+using a suitably crafted dictionary, with dictionary expansion, as the
+``**kwargs`` passed to :meth:`.QuerySet.annotate` or :meth:`.QuerySet.alias` on
+PostgreSQL.
+
Bugfixes
========
diff --git a/docs/releases/5.1.15.txt b/docs/releases/5.1.15.txt
index 2c4e02959031..f55623ea9684 100644
--- a/docs/releases/5.1.15.txt
+++ b/docs/releases/5.1.15.txt
@@ -7,6 +7,14 @@ Django 5.1.15 release notes
Django 5.1.15 fixes one security issue with severity "high", one security issue
with severity "moderate", and one bug in 5.1.14.
+CVE-2025-13372: Potential SQL injection in ``FilteredRelation`` column aliases on PostgreSQL
+============================================================================================
+
+:class:`.FilteredRelation` was subject to SQL injection in column aliases,
+using a suitably crafted dictionary, with dictionary expansion, as the
+``**kwargs`` passed to :meth:`.QuerySet.annotate` or :meth:`.QuerySet.alias` on
+PostgreSQL.
+
Bugfixes
========
diff --git a/docs/releases/5.2.9.txt b/docs/releases/5.2.9.txt
index 9dfcc392a036..08c298999a56 100644
--- a/docs/releases/5.2.9.txt
+++ b/docs/releases/5.2.9.txt
@@ -7,6 +7,14 @@ Django 5.2.9 release notes
Django 5.2.9 fixes one security issue with severity "high", one security issue
with severity "moderate", and several bugs in 5.2.8.
+CVE-2025-13372: Potential SQL injection in ``FilteredRelation`` column aliases on PostgreSQL
+============================================================================================
+
+:class:`.FilteredRelation` was subject to SQL injection in column aliases,
+using a suitably crafted dictionary, with dictionary expansion, as the
+``**kwargs`` passed to :meth:`.QuerySet.annotate` or :meth:`.QuerySet.alias` on
+PostgreSQL.
+
Bugfixes
========
diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py
index a114480d48e6..10cd05db63f4 100644
--- a/tests/annotations/tests.py
+++ b/tests/annotations/tests.py
@@ -1541,6 +1541,17 @@ def test_alias_filtered_relation_sql_injection(self):
with self.assertRaisesMessage(ValueError, msg):
Book.objects.alias(**{crafted_alias: FilteredRelation("authors")})
+ def test_alias_filtered_relation_sql_injection_dollar_sign(self):
+ qs = Book.objects.alias(
+ **{"crafted_alia$": FilteredRelation("authors")}
+ ).values("name", "crafted_alia$")
+ if connection.vendor == "postgresql":
+ msg = "Dollar signs are not permitted in column aliases on PostgreSQL."
+ with self.assertRaisesMessage(ValueError, msg):
+ list(qs)
+ else:
+ self.assertEqual(qs.first()["name"], self.b1.name)
+
def test_values_wrong_alias(self):
expected_message = (
"Cannot resolve keyword 'alias_typo' into field. Choices are: %s"