From 69065ca869b0970dff8fdd8fafb390bf8b3bf222 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 21 Jan 2026 17:53:52 -0500 Subject: Fixed CVE-2026-1312 -- Protected order_by() from SQL injection via aliases with periods. Before, `order_by()` treated a period in a field name as a sign that it was requested via `.extra(order_by=...)` and thus should be passed through as raw table and column names, even if `extra()` was not used. Since periods are permitted in aliases, this meant user-controlled aliases could force the `order_by()` clause to resolve to a raw table and column pair instead of the actual target field for the alias. In practice, only `FilteredRelation` was affected, as the other expressions we tested, e.g. `F`, aggressively optimize away the ordering expressions into ordinal positions, e.g. ORDER BY 2, instead of ORDER BY "table".column. Thanks Solomon Kebede for the report, and Simon Charette and Jake Howard for reviews. --- django/db/models/sql/compiler.py | 2 +- docs/releases/4.2.28.txt | 10 ++++++++++ docs/releases/5.2.11.txt | 10 ++++++++++ docs/releases/6.0.2.txt | 10 ++++++++++ tests/ordering/tests.py | 25 +++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 1b1cd91c9a..8742de00d6 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -433,7 +433,7 @@ class SQLCompiler: yield OrderBy(expr, descending=descending), False continue - if "." in field: + if "." in field and field in self.query.extra_order_by: # This came in through an extra(order_by=...) addition. Pass it # on verbatim. table, col = col.split(".", 1) diff --git a/docs/releases/4.2.28.txt b/docs/releases/4.2.28.txt index 473e44f577..1d81095b3e 100644 --- a/docs/releases/4.2.28.txt +++ b/docs/releases/4.2.28.txt @@ -66,3 +66,13 @@ expansion, as the ``**kwargs`` passed to :meth:`.QuerySet.annotate`, This issue has severity "high" according to the :ref:`Django security policy `. + +CVE-2026-1312: Potential SQL injection via ``QuerySet.order_by`` and ``FilteredRelation`` +========================================================================================= + +:meth:`.QuerySet.order_by` was subject to SQL injection in column aliases +containing periods when the same alias was, using a suitably crafted +dictionary, with dictionary expansion, used in :class:`.FilteredRelation`. + +This issue has severity "high" according to the :ref:`Django security policy +`. diff --git a/docs/releases/5.2.11.txt b/docs/releases/5.2.11.txt index fa14a88c0a..76efc4aa8d 100644 --- a/docs/releases/5.2.11.txt +++ b/docs/releases/5.2.11.txt @@ -66,3 +66,13 @@ expansion, as the ``**kwargs`` passed to :meth:`.QuerySet.annotate`, This issue has severity "high" according to the :ref:`Django security policy `. + +CVE-2026-1312: Potential SQL injection via ``QuerySet.order_by`` and ``FilteredRelation`` +========================================================================================= + +:meth:`.QuerySet.order_by` was subject to SQL injection in column aliases +containing periods when the same alias was, using a suitably crafted +dictionary, with dictionary expansion, used in :class:`.FilteredRelation`. + +This issue has severity "high" according to the :ref:`Django security policy +`. diff --git a/docs/releases/6.0.2.txt b/docs/releases/6.0.2.txt index 884c873a6d..0372cf9eb0 100644 --- a/docs/releases/6.0.2.txt +++ b/docs/releases/6.0.2.txt @@ -67,6 +67,16 @@ expansion, as the ``**kwargs`` passed to :meth:`.QuerySet.annotate`, This issue has severity "high" according to the :ref:`Django security policy `. +CVE-2026-1312: Potential SQL injection via ``QuerySet.order_by`` and ``FilteredRelation`` +========================================================================================= + +:meth:`.QuerySet.order_by` was subject to SQL injection in column aliases +containing periods when the same alias was, using a suitably crafted +dictionary, with dictionary expansion, used in :class:`.FilteredRelation`. + +This issue has severity "high" according to the :ref:`Django security policy +`. + Bugfixes ======== diff --git a/tests/ordering/tests.py b/tests/ordering/tests.py index 421689b9fa..afe2e3c22c 100644 --- a/tests/ordering/tests.py +++ b/tests/ordering/tests.py @@ -8,6 +8,7 @@ from django.db.models import ( Count, DateTimeField, F, + FilteredRelation, IntegerField, Max, OrderBy, @@ -17,6 +18,7 @@ from django.db.models import ( When, ) from django.db.models.functions import Length, Upper +from django.db.utils import DatabaseError from django.test import TestCase from .models import ( @@ -395,6 +397,29 @@ class OrderingTests(TestCase): attrgetter("headline"), ) + def test_alias_with_period_shadows_table_name(self): + """ + Aliases with periods are not confused for table names from extra(). + """ + Article.objects.update(author=self.author_2) + Article.objects.create( + headline="Backdated", pub_date=datetime(1900, 1, 1), author=self.author_1 + ) + crafted = "ordering_article.pub_date" + + qs = Article.objects.annotate(**{crafted: F("author")}).order_by("-" + crafted) + self.assertNotEqual(qs[0].headline, "Backdated") + + relation = FilteredRelation("author") + qs2 = Article.objects.annotate(**{crafted: relation}).order_by(crafted) + with self.assertRaises(DatabaseError): + # Before, unlike F(), which causes ordering expressions to be + # replaced by ordinals like n in ORDER BY n, these were ordered by + # pub_date instead of author. + # The Article model orders by -pk, so sorting on author will place + # first any article by author2 instead of the backdated one. + self.assertNotEqual(qs2[0].headline, "Backdated") + def test_order_by_pk(self): """ 'pk' works as an ordering option in Meta. -- cgit v1.3