Fixed CVE-2022-28346 -- Protected QuerySet.annotate(), aggregate(), and extra() against SQL injection in column aliases.

Thanks Splunk team: Preston Elder, Jacob Davis, Jacob Moore,
Matt Hanson, David Briggs, and a security researcher: Danylo Dmytriiev
(DDV_UA) for the report.
This commit is contained in:
Mariusz Felisiak 2022-04-01 08:10:22 +02:00
parent 62739b6e26
commit 93cae5cb2f
8 changed files with 108 additions and 0 deletions

View File

@ -40,10 +40,15 @@ 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.datastructures import BaseTable, Empty, Join, MultiJoin
from django.db.models.sql.where import AND, OR, ExtraWhere, NothingNode, WhereNode from django.db.models.sql.where import AND, OR, ExtraWhere, NothingNode, WhereNode
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.regex_helper import _lazy_re_compile
from django.utils.tree import Node from django.utils.tree import Node
__all__ = ["Query", "RawQuery"] __all__ = ["Query", "RawQuery"]
# Quotation marks ('"`[]), whitespace characters, semicolons, or inline
# SQL comments are forbidden in column aliases.
FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|--|/\*|\*/")
def get_field_names_from_opts(opts): def get_field_names_from_opts(opts):
if opts is None: if opts is None:
@ -1091,8 +1096,16 @@ class Query(BaseExpression):
alias = seen[int_model] = join_info.joins[-1] alias = seen[int_model] = join_info.joins[-1]
return alias or seen[None] return alias or seen[None]
def check_alias(self, alias):
if FORBIDDEN_ALIAS_PATTERN.search(alias):
raise ValueError(
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
)
def add_annotation(self, annotation, alias, is_summary=False, select=True): def add_annotation(self, annotation, alias, is_summary=False, select=True):
"""Add a single annotation expression to the Query.""" """Add a single annotation expression to the Query."""
self.check_alias(alias)
annotation = annotation.resolve_expression( annotation = annotation.resolve_expression(
self, allow_joins=True, reuse=None, summarize=is_summary self, allow_joins=True, reuse=None, summarize=is_summary
) )
@ -2269,6 +2282,7 @@ class Query(BaseExpression):
else: else:
param_iter = iter([]) param_iter = iter([])
for name, entry in select.items(): for name, entry in select.items():
self.check_alias(name)
entry = str(entry) entry = str(entry)
entry_params = [] entry_params = []
pos = entry.find("%s") pos = entry.find("%s")

View File

@ -5,3 +5,11 @@ Django 2.2.28 release notes
*April 11, 2022* *April 11, 2022*
Django 2.2.28 fixes two security issues with severity "high" in 2.2.27. Django 2.2.28 fixes two security issues with severity "high" in 2.2.27.
CVE-2022-28346: Potential SQL injection in ``QuerySet.annotate()``, ``aggregate()``, and ``extra()``
====================================================================================================
:meth:`.QuerySet.annotate`, :meth:`~.QuerySet.aggregate`, and
:meth:`~.QuerySet.extra` methods were subject to SQL injection in column
aliases, using a suitably crafted dictionary, with dictionary expansion, as the
``**kwargs`` passed to these methods.

View File

@ -7,6 +7,14 @@ Django 3.2.13 release notes
Django 3.2.13 fixes two security issues with severity "high" in Django 3.2.13 fixes two security issues with severity "high" in
3.2.12 and a regression in 3.2.4. 3.2.12 and a regression in 3.2.4.
CVE-2022-28346: Potential SQL injection in ``QuerySet.annotate()``, ``aggregate()``, and ``extra()``
====================================================================================================
:meth:`.QuerySet.annotate`, :meth:`~.QuerySet.aggregate`, and
:meth:`~.QuerySet.extra` methods were subject to SQL injection in column
aliases, using a suitably crafted dictionary, with dictionary expansion, as the
``**kwargs`` passed to these methods.
Bugfixes Bugfixes
======== ========

View File

@ -7,6 +7,14 @@ Django 4.0.4 release notes
Django 4.0.4 fixes two security issues with severity "high" and two bugs in Django 4.0.4 fixes two security issues with severity "high" and two bugs in
4.0.3. 4.0.3.
CVE-2022-28346: Potential SQL injection in ``QuerySet.annotate()``, ``aggregate()``, and ``extra()``
====================================================================================================
:meth:`.QuerySet.annotate`, :meth:`~.QuerySet.aggregate`, and
:meth:`~.QuerySet.extra` methods were subject to SQL injection in column
aliases, using a suitably crafted dictionary, with dictionary expansion, as the
``**kwargs`` passed to these methods.
Bugfixes Bugfixes
======== ========

View File

@ -2048,6 +2048,15 @@ class AggregateTestCase(TestCase):
) )
self.assertEqual(len(qs), 6) 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, quotation marks, "
"semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Author.objects.aggregate(**{crafted_alias: Avg("age")})
def test_exists_extra_where_with_aggregate(self): def test_exists_extra_where_with_aggregate(self):
qs = Book.objects.annotate( qs = Book.objects.annotate(
count=Count("id"), count=Count("id"),

View File

@ -1076,6 +1076,40 @@ class NonAggregateAnnotationTestCase(TestCase):
], ],
) )
def test_alias_sql_injection(self):
crafted_alias = """injected_name" from "annotations_book"; --"""
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Book.objects.annotate(**{crafted_alias: Value(1)})
def test_alias_forbidden_chars(self):
tests = [
'al"ias',
"a'lias",
"ali`as",
"alia s",
"alias\t",
"ali\nas",
"alias--",
"ali/*as",
"alias*/",
"alias;",
# [] are used by MSSQL.
"alias[",
"alias]",
]
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
)
for crafted_alias in tests:
with self.subTest(crafted_alias):
with self.assertRaisesMessage(ValueError, msg):
Book.objects.annotate(**{crafted_alias: Value(1)})
class AliasTests(TestCase): class AliasTests(TestCase):
@classmethod @classmethod
@ -1339,3 +1373,12 @@ class AliasTests(TestCase):
with self.subTest(operation=operation): with self.subTest(operation=operation):
with self.assertRaisesMessage(FieldError, msg): with self.assertRaisesMessage(FieldError, msg):
getattr(qs, operation)("rating_alias") getattr(qs, operation)("rating_alias")
def test_alias_sql_injection(self):
crafted_alias = """injected_name" from "annotations_book"; --"""
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Book.objects.alias(**{crafted_alias: Value(1)})

View File

@ -34,6 +34,15 @@ class ValuesExpressionsTests(TestCase):
[{"salary": 10}, {"salary": 20}, {"salary": 30}], [{"salary": 10}, {"salary": 20}, {"salary": 30}],
) )
def test_values_expression_alias_sql_injection(self):
crafted_alias = """injected_name" from "expressions_company"; --"""
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Company.objects.values(**{crafted_alias: F("ceo__salary")})
def test_values_expression_group_by(self): def test_values_expression_group_by(self):
# values() applies annotate() first, so values selected are grouped by # values() applies annotate() first, so values selected are grouped by
# id, not firstname. # id, not firstname.

View File

@ -1898,6 +1898,15 @@ class Queries5Tests(TestCase):
Note.objects.extra(select={"foo": "'bar %%s'"})[0].foo, "bar %s" Note.objects.extra(select={"foo": "'bar %%s'"})[0].foo, "bar %s"
) )
def test_extra_select_alias_sql_injection(self):
crafted_alias = """injected_name" from "queries_note"; --"""
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Note.objects.extra(select={crafted_alias: "1"})
def test_queryset_reuse(self): def test_queryset_reuse(self):
# Using querysets doesn't mutate aliases. # Using querysets doesn't mutate aliases.
authors = Author.objects.filter(Q(name="a1") | Q(name="nonexistent")) authors = Author.objects.filter(Q(name="a1") | Q(name="nonexistent"))