SQL Injection via FilteredRelation column aliases on PostgreSQL

HIGH
django/django
Commit: 5b90ca1e7591
Affected: 5.1.x before 5.1.15 (i.e., 5.1.0 through 5.1.14)
2026-04-05 13:42 UTC

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"
← Back to Alerts View on GitHub →