From 9c6a5bde24240382807d13bc3748d08444709355 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 17 Nov 2025 17:09:54 -0500 Subject: [PATCH] [5.1.x] 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. Backport of 5b90ca1e7591fa36fccf2d6dad67cf1477e6293e from main. --- django/db/backends/postgresql/compiler.py | 24 +++++++++++++++++++++ django/db/backends/postgresql/operations.py | 1 + docs/releases/4.2.27.txt | 8 +++++++ docs/releases/5.1.15.txt | 8 +++++++ tests/annotations/tests.py | 12 +++++++++++ 5 files changed, 53 insertions(+) create mode 100644 django/db/backends/postgresql/compiler.py diff --git a/django/db/backends/postgresql/compiler.py b/django/db/backends/postgresql/compiler.py new file mode 100644 index 0000000000..d4140c7f98 --- /dev/null +++ b/django/db/backends/postgresql/compiler.py @@ -0,0 +1,24 @@ +from django.db.models.sql.compiler import ( # isort:skip + SQLAggregateCompiler, + SQLCompiler as BaseSQLCompiler, + SQLDeleteCompiler, + SQLInsertCompiler, + SQLUpdateCompiler, +) + +__all__ = [ + "SQLAggregateCompiler", + "SQLCompiler", + "SQLDeleteCompiler", + "SQLInsertCompiler", + "SQLUpdateCompiler", +] + + +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) diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index 4b179ca83f..5abc91f79c 100644 --- a/django/db/backends/postgresql/operations.py +++ b/django/db/backends/postgresql/operations.py @@ -24,6 +24,7 @@ def get_json_dumps(encoder): class DatabaseOperations(BaseDatabaseOperations): + compiler_module = "django.db.backends.postgresql.compiler" cast_char_field_without_max_length = "varchar" explain_prefix = "EXPLAIN" explain_options = frozenset( diff --git a/docs/releases/4.2.27.txt b/docs/releases/4.2.27.txt index 7ffa5fa458..e95dc63f74 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 2c4e029590..f55623ea96 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/tests/annotations/tests.py b/tests/annotations/tests.py index ac40408977..04f1b5bea7 100644 --- a/tests/annotations/tests.py +++ b/tests/annotations/tests.py @@ -2,6 +2,7 @@ import datetime from decimal import Decimal from django.core.exceptions import FieldDoesNotExist, FieldError +from django.db import connection from django.db.models import ( BooleanField, Case, @@ -1454,3 +1455,14 @@ class AliasTests(TestCase): ) 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)