Potential SQL injection in column aliases via control characters
Description
An issue was discovered in 6.0 before 6.0.2, 5.2 before 5.2.11, and 4.2 before 4.2.28. FilteredRelation is subject to SQL injection in column aliases via control characters, using a suitably crafted dictionary, with dictionary expansion, as the **kwargs passed to QuerySet methods annotate(), aggregate(), extra(), values(), values_list(), and alias(). Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected. Django would like to thank Solomon Kebede for reporting this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
DjangoPyPI | >= 6.0a1, < 6.0.2 | 6.0.2 |
DjangoPyPI | >= 5.2a1, < 5.2.11 | 5.2.11 |
DjangoPyPI | >= 4.2a1, < 4.2.28 | 4.2.28 |
Affected products
1- Range: 4.2, 4.2.1, 4.2.10, …
Patches
1e891a84c7ef9Fixed CVE-2026-1287 -- Protected against SQL injection in column aliases via control characters.
8 files changed · +149 −59
django/db/models/sql/query.py+14 −9 modified@@ -51,12 +51,17 @@ __all__ = ["Query", "RawQuery"] # RemovedInDjango70Warning: When the deprecation ends, replace with: -# Quotation marks ('"`[]), whitespace characters, semicolons, percent signs, -# hashes, or inline SQL comments are forbidden in column aliases. -# FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|%|#|--|/\*|\*/") -# Quotation marks ('"`[]), whitespace characters, semicolons, hashes, or inline -# SQL comments are forbidden in column aliases. -FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|#|--|/\*|\*/") +# Quotation marks ('"`[]), whitespace characters, control characters, +# semicolons, percent signs, hashes, or inline SQL comments are +# forbidden in column aliases. +# FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile( +# r"['`\"\]\[;\s\x00-\x1F\x7F-\x9F]|%|#|--|/\*|\*/" +# ) +# Quotation marks ('"`[]), whitespace characters, control characters, +# semicolons, hashes, or inline SQL comments are forbidden in column aliases. +FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile( + r"['`\"\]\[;\s\x00-\x1F\x7F-\x9F]|#|--|/\*|\*/" +) # Inspired from # https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS @@ -1226,9 +1231,9 @@ def check_alias(self, alias): "Column aliases cannot contain whitespace characters, hashes, " # RemovedInDjango70Warning: When the deprecation ends, replace # with: - # "quotation marks, semicolons, percent signs, or SQL " - # "comments." - "quotation marks, semicolons, or SQL comments." + # "control characters, quotation marks, semicolons, percent " + # "signs, or SQL comments." + "control characters, quotation marks, semicolons, or SQL comments." ) def add_annotation(self, annotation, alias, select=True):
docs/releases/4.2.28.txt+13 −0 modified@@ -53,3 +53,16 @@ HTML end tags, which could cause quadratic time complexity during HTML parsing. This issue has severity "moderate" according to the :ref:`Django security policy <security-disclosure>`. + +CVE-2026-1287: Potential SQL injection in column aliases via control characters +=============================================================================== + +:class:`.FilteredRelation` was subject to SQL injection in column aliases via +control characters, using a suitably crafted dictionary, with dictionary +expansion, as the ``**kwargs`` passed to :meth:`.QuerySet.annotate`, +:meth:`~.QuerySet.aggregate`, :meth:`~.QuerySet.extra`, +:meth:`~.QuerySet.values`, :meth:`~.QuerySet.values_list`, and +:meth:`~.QuerySet.alias`. + +This issue has severity "high" according to the :ref:`Django security policy +<security-disclosure>`.
docs/releases/5.2.11.txt+13 −0 modified@@ -53,3 +53,16 @@ HTML end tags, which could cause quadratic time complexity during HTML parsing. This issue has severity "moderate" according to the :ref:`Django security policy <security-disclosure>`. + +CVE-2026-1287: Potential SQL injection in column aliases via control characters +=============================================================================== + +:class:`.FilteredRelation` was subject to SQL injection in column aliases via +control characters, using a suitably crafted dictionary, with dictionary +expansion, as the ``**kwargs`` passed to :meth:`.QuerySet.annotate`, +:meth:`~.QuerySet.aggregate`, :meth:`~.QuerySet.extra`, +:meth:`~.QuerySet.values`, :meth:`~.QuerySet.values_list`, and +:meth:`~.QuerySet.alias`. + +This issue has severity "high" according to the :ref:`Django security policy +<security-disclosure>`.
docs/releases/6.0.2.txt+13 −0 modified@@ -54,6 +54,19 @@ HTML end tags, which could cause quadratic time complexity during HTML parsing. This issue has severity "moderate" according to the :ref:`Django security policy <security-disclosure>`. +CVE-2026-1287: Potential SQL injection in column aliases via control characters +=============================================================================== + +:class:`.FilteredRelation` was subject to SQL injection in column aliases via +control characters, using a suitably crafted dictionary, with dictionary +expansion, as the ``**kwargs`` passed to :meth:`.QuerySet.annotate`, +:meth:`~.QuerySet.aggregate`, :meth:`~.QuerySet.extra`, +:meth:`~.QuerySet.values`, :meth:`~.QuerySet.values_list`, and +:meth:`~.QuerySet.alias`. + +This issue has severity "high" according to the :ref:`Django security policy +<security-disclosure>`. + Bugfixes ========
tests/aggregation/tests.py+12 −6 modified@@ -2,6 +2,7 @@ import math import re from decimal import Decimal +from itertools import chain from django.core.exceptions import FieldError from django.db import NotSupportedError, connection @@ -2242,13 +2243,18 @@ def test_exists_none_with_aggregate(self): self.assertEqual(len(qs), 6) def test_alias_sql_injection(self): - crafted_alias = """injected_name" from "aggregation_author"; --""" msg = ( - "Column aliases cannot contain whitespace characters, hashes, quotation " - "marks, semicolons, or SQL comments." - ) - with self.assertRaisesMessage(ValueError, msg): - Author.objects.aggregate(**{crafted_alias: Avg("age")}) + "Column aliases cannot contain whitespace characters, hashes, " + "control characters, quotation marks, semicolons, or SQL comments." + ) + for crafted_alias in [ + """injected_name" from "aggregation_author"; --""", + # Control characters. + *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))), + ]: + with self.subTest(crafted_alias): + with self.assertRaisesMessage(ValueError, msg): + Author.objects.aggregate(**{crafted_alias: Avg("age")}) def test_exists_extra_where_with_aggregate(self): qs = Book.objects.annotate(
tests/annotations/tests.py+48 −26 modified@@ -1,5 +1,6 @@ import datetime from decimal import Decimal +from itertools import chain from unittest import skipUnless from django.core.exceptions import FieldDoesNotExist, FieldError @@ -1169,32 +1170,42 @@ def test_annotation_aggregate_with_m2o(self): ) def test_alias_sql_injection(self): - crafted_alias = """injected_name" from "annotations_book"; --""" # RemovedInDjango70Warning: When the deprecation ends, replace with: # msg = ( # "Column aliases cannot contain whitespace characters, hashes, " # "quotation marks, semicolons, percent signs, or SQL comments." # ) msg = ( - "Column aliases cannot contain whitespace characters, hashes, quotation " - "marks, semicolons, or SQL comments." - ) - with self.assertRaisesMessage(ValueError, msg): - Book.objects.annotate(**{crafted_alias: Value(1)}) + "Column aliases cannot contain whitespace characters, hashes, " + "control characters, quotation marks, semicolons, or SQL comments." + ) + for crafted_alias in [ + """injected_name" from "annotations_book"; --""", + # Control characters. + *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))), + ]: + with self.subTest(crafted_alias): + with self.assertRaisesMessage(ValueError, msg): + Book.objects.annotate(**{crafted_alias: Value(1)}) def test_alias_filtered_relation_sql_injection(self): - crafted_alias = """injected_name" from "annotations_book"; --""" # RemovedInDjango70Warning: When the deprecation ends, replace with: # msg = ( # "Column aliases cannot contain whitespace characters, hashes, " # "quotation marks, semicolons, percent signs, or SQL comments." # ) msg = ( - "Column aliases cannot contain whitespace characters, hashes, quotation " - "marks, semicolons, or SQL comments." - ) - with self.assertRaisesMessage(ValueError, msg): - Book.objects.annotate(**{crafted_alias: FilteredRelation("author")}) + "Column aliases cannot contain whitespace characters, hashes, " + "control characters, quotation marks, semicolons, or SQL comments." + ) + for crafted_alias in [ + """injected_name" from "annotations_book"; --""", + # Control characters. + *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))), + ]: + with self.subTest(crafted_alias): + with self.assertRaisesMessage(ValueError, msg): + Book.objects.annotate(**{crafted_alias: FilteredRelation("author")}) def test_alias_forbidden_chars(self): tests = [ @@ -1214,15 +1225,16 @@ def test_alias_forbidden_chars(self): "alias[", "alias]", "ali#as", + "ali\0as", ] # RemovedInDjango70Warning: When the deprecation ends, replace with: # msg = ( # "Column aliases cannot contain whitespace characters, hashes, " # "quotation marks, semicolons, percent signs, or SQL comments." # ) msg = ( - "Column aliases cannot contain whitespace characters, hashes, quotation " - "marks, semicolons, or SQL comments." + "Column aliases cannot contain whitespace characters, hashes, " + "control characters, quotation marks, semicolons, or SQL comments." ) for crafted_alias in tests: with self.subTest(crafted_alias): @@ -1525,32 +1537,42 @@ def test_alias_after_values(self): self.assertEqual(qs.get(pk=self.b1.pk), (self.b1.pk,)) def test_alias_sql_injection(self): - crafted_alias = """injected_name" from "annotations_book"; --""" # RemovedInDjango70Warning: When the deprecation ends, replace with: # msg = ( # "Column aliases cannot contain whitespace characters, hashes, " # "quotation marks, semicolons, percent signs, or SQL comments." # ) msg = ( - "Column aliases cannot contain whitespace characters, hashes, quotation " - "marks, semicolons, or SQL comments." - ) - with self.assertRaisesMessage(ValueError, msg): - Book.objects.alias(**{crafted_alias: Value(1)}) + "Column aliases cannot contain whitespace characters, hashes, " + "control characters, quotation marks, semicolons, or SQL comments." + ) + for crafted_alias in [ + """injected_name" from "annotations_book"; --""", + # Control characters. + *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))), + ]: + with self.subTest(crafted_alias): + with self.assertRaisesMessage(ValueError, msg): + Book.objects.alias(**{crafted_alias: Value(1)}) def test_alias_filtered_relation_sql_injection(self): - crafted_alias = """injected_name" from "annotations_book"; --""" # RemovedInDjango70Warning: When the deprecation ends, replace with: # msg = ( # "Column aliases cannot contain whitespace characters, hashes, " # "quotation marks, semicolons, percent signs, or SQL comments." # ) msg = ( - "Column aliases cannot contain whitespace characters, hashes, quotation " - "marks, semicolons, or SQL comments." - ) - with self.assertRaisesMessage(ValueError, msg): - Book.objects.alias(**{crafted_alias: FilteredRelation("authors")}) + "Column aliases cannot contain whitespace characters, hashes, " + "control characters, quotation marks, semicolons, or SQL comments." + ) + for crafted_alias in [ + """injected_name" from "annotations_book"; --""", + # Control characters. + *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))), + ]: + with self.subTest(crafted_alias): + 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(
tests/expressions/test_queryset_values.py+24 −12 modified@@ -1,3 +1,5 @@ +from itertools import chain + from django.db.models import F, Sum from django.test import TestCase, skipUnlessDBFeature from django.utils.deprecation import RemovedInDjango70Warning @@ -42,26 +44,36 @@ def test_values_expression_containing_percent_sign_deprecation_warns_once(self): self.assertEqual(len(cm.warnings), 1) def test_values_expression_alias_sql_injection(self): - crafted_alias = """injected_name" from "expressions_company"; --""" msg = ( - "Column aliases cannot contain whitespace characters, hashes, quotation " - "marks, semicolons, or SQL comments." + "Column aliases cannot contain whitespace characters, hashes, " + "control characters, quotation marks, semicolons, or SQL comments." ) - with self.assertRaisesMessage(ValueError, msg): - Company.objects.values(**{crafted_alias: F("ceo__salary")}) + for crafted_alias in [ + """injected_name" from "expressions_company"; --""", + # Control characters. + *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))), + ]: + with self.subTest(crafted_alias): + with self.assertRaisesMessage(ValueError, msg): + Company.objects.values(**{crafted_alias: F("ceo__salary")}) @skipUnlessDBFeature("supports_json_field") def test_values_expression_alias_sql_injection_json_field(self): - crafted_alias = """injected_name" from "expressions_company"; --""" msg = ( - "Column aliases cannot contain whitespace characters, hashes, quotation " - "marks, semicolons, or SQL comments." + "Column aliases cannot contain whitespace characters, hashes, " + "control characters, quotation marks, semicolons, or SQL comments." ) - with self.assertRaisesMessage(ValueError, msg): - JSONFieldModel.objects.values(f"data__{crafted_alias}") + for crafted_alias in [ + """injected_name" from "expressions_company"; --""", + # Control characters. + *(chr(c) for c in chain(range(32), range(0x7F, 0xA0))), + ]: + with self.subTest(crafted_alias): + with self.assertRaisesMessage(ValueError, msg): + JSONFieldModel.objects.values(f"data__{crafted_alias}") - with self.assertRaisesMessage(ValueError, msg): - JSONFieldModel.objects.values_list(f"data__{crafted_alias}") + with self.assertRaisesMessage(ValueError, msg): + JSONFieldModel.objects.values_list(f"data__{crafted_alias}") def test_values_expression_group_by(self): # values() applies annotate() first, so values selected are grouped by
tests/queries/tests.py+12 −6 modified@@ -2,6 +2,7 @@ import pickle import sys import unittest +from itertools import chain from operator import attrgetter from django.core.exceptions import EmptyResultSet, FieldError, FullResultSet @@ -1965,13 +1966,18 @@ def test_extra_select_literal_percent_s(self): ) def test_extra_select_alias_sql_injection(self): - crafted_alias = """injected_name" from "queries_note"; --""" msg = ( - "Column aliases cannot contain whitespace characters, hashes, quotation " - "marks, semicolons, or SQL comments." - ) - with self.assertRaisesMessage(ValueError, msg): - Note.objects.extra(select={crafted_alias: "1"}) + "Column aliases cannot contain whitespace characters, hashes, " + "control characters, quotation marks, semicolons, or SQL comments." + ) + for crafted_alias in [ + """injected_name" from "queries_note"; --""", + # Control characters. + *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))), + ]: + with self.subTest(crafted_alias): + with self.assertRaisesMessage(ValueError, msg): + Note.objects.extra(select={crafted_alias: "1"}) def test_queryset_reuse(self): # Using querysets doesn't mutate aliases.
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- docs.djangoproject.com/en/dev/releases/security/mitrevendor-advisory
- github.com/advisories/GHSA-gvg8-93h5-g6qqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-1287ghsaADVISORY
- www.djangoproject.com/weblog/2026/feb/03/security-releases/mitrevendor-advisory
- docs.djangoproject.com/en/dev/releases/securityghsaWEB
- github.com/django/django/commit/e891a84c7ef9962bfcc3b4685690219542f86a22ghsaWEB
- groups.google.com/g/django-announceghsamailing-listWEB
- www.djangoproject.com/weblog/2026/feb/03/security-releasesghsaWEB
News mentions
1- How AI Assistants are Moving the Security GoalpostsKrebs on Security · Mar 8, 2026