diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 187349a460..20dbf7cfaa 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -10,7 +10,9 @@ all about the internals of models in order to get the information it needs. import copy import difflib import functools +import inspect import sys +import warnings from collections import Counter, namedtuple from collections.abc import Iterator, Mapping from itertools import chain, count, product @@ -42,12 +44,17 @@ from django.db.models.query_utils import ( from django.db.models.sql.constants import INNER, LOUTER, ORDER_DIR, SINGLE from django.db.models.sql.datastructures import BaseTable, Empty, Join, MultiJoin from django.db.models.sql.where import AND, OR, ExtraWhere, NothingNode, WhereNode +from django.utils.deprecation import RemovedInDjango70Warning from django.utils.functional import cached_property from django.utils.regex_helper import _lazy_re_compile from django.utils.tree import Node __all__ = ["Query", "RawQuery"] +# RemovedInDjango70Warning: When the deprecation ends, replace with: +# Quotation marks ('"`[]), whitespace characters, semicolons, percent signs +# or inline SQL comments are forbidden in column aliases. +# FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|%|--|/\*|\*/") # Quotation marks ('"`[]), whitespace characters, semicolons, or inline # SQL comments are forbidden in column aliases. FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|--|/\*|\*/") @@ -1206,9 +1213,23 @@ class Query(BaseExpression): return alias or seen[None] def check_alias(self, alias): + # RemovedInDjango70Warning: When the deprecation ends, remove. + if "%" in alias: + if "aggregate" in {frame.function for frame in inspect.stack()}: + stacklevel = 5 + else: + # annotate() and alias(). + stacklevel = 6 + warnings.warn( + "Using percent signs in a column alias is deprecated.", + stacklevel=stacklevel, + category=RemovedInDjango70Warning, + ) if FORBIDDEN_ALIAS_PATTERN.search(alias): raise ValueError( "Column aliases cannot contain whitespace characters, quotation marks, " + # RemovedInDjango70Warning: When the deprecation ends, replace with: + # "semicolons, percent signs, or SQL comments." "semicolons, or SQL comments." ) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index e6db513dae..fb37d43849 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -28,6 +28,9 @@ details on these changes. * The ``URLIZE_ASSUME_HTTPS`` transitional setting will be removed. +* Using a percent sign in a column alias or annotation will raise + ``ValueError``. + * Support for setting the ``ADMINS`` or ``MANAGERS`` settings to a list of (name, address) tuples will be removed. diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index 362ea70ea0..304343256f 100644 --- a/docs/releases/6.0.txt +++ b/docs/releases/6.0.txt @@ -375,6 +375,8 @@ Miscellaneous ``per_page`` argument of :class:`django.core.paginator.Paginator` and :class:`django.core.paginator.AsyncPaginator` is deprecated. +* Using a percent sign in a column alias or annotation is deprecated. + Features removed in 6.0 ======================= diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py index 6c0d7b668c..eb077fcb57 100644 --- a/tests/annotations/tests.py +++ b/tests/annotations/tests.py @@ -39,6 +39,7 @@ from django.db.models.functions import ( from django.db.models.sql.query import get_field_names_from_opts from django.test import TestCase, skipUnlessDBFeature from django.test.utils import register_lookup +from django.utils.deprecation import RemovedInDjango70Warning from .models import ( Author, @@ -1157,6 +1158,11 @@ class NonAggregateAnnotationTestCase(TestCase): 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, quotation marks, " + # "semicolons, percent signs, or SQL comments." + # ) msg = ( "Column aliases cannot contain whitespace characters, quotation marks, " "semicolons, or SQL comments." @@ -1176,10 +1182,17 @@ class NonAggregateAnnotationTestCase(TestCase): "ali/*as", "alias*/", "alias;", + # RemovedInDjango70Warning: When the deprecation ends, add this case. + # "alias%", # [] are used by MSSQL. "alias[", "alias]", ] + # RemovedInDjango70Warning: When the deprecation ends, replace with: + # msg = ( + # "Column aliases cannot contain whitespace characters, quotation marks, " + # "semicolons, percent signs, or SQL comments." + # ) msg = ( "Column aliases cannot contain whitespace characters, quotation marks, " "semicolons, or SQL comments." @@ -1189,6 +1202,11 @@ class NonAggregateAnnotationTestCase(TestCase): with self.assertRaisesMessage(ValueError, msg): Book.objects.annotate(**{crafted_alias: Value(1)}) + def test_alias_containing_percent_sign_deprecation(self): + msg = "Using percent signs in a column alias is deprecated." + with self.assertRaisesMessage(RemovedInDjango70Warning, msg): + Book.objects.annotate(**{"alias%": Value(1)}) + @skipUnless(connection.vendor == "postgresql", "PostgreSQL tests") @skipUnlessDBFeature("supports_json_field") def test_set_returning_functions(self): @@ -1476,6 +1494,11 @@ class AliasTests(TestCase): 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, quotation marks, " + # "semicolons, percent signs, or SQL comments." + # ) msg = ( "Column aliases cannot contain whitespace characters, quotation marks, " "semicolons, or SQL comments."