From 1ea262db1c9ad5572bc00a3faab6178155ee3e41 Mon Sep 17 00:00:00 2001 From: starlet-dx <15929766099@163.com> Date: Thu, 21 Apr 2022 11:11:20 +0800 Subject: [PATCH] Fix CVE-2022-28346 CVE-2022-28347 (cherry picked from commit 4de4eb160f57756e94ce5a5633d59a0044bf61b7) --- CVE-2022-28346.patch | 168 +++++++++++++++++++++++++++++++++++++++++++ CVE-2022-28347.patch | 155 +++++++++++++++++++++++++++++++++++++++ python-django.spec | 13 +++- 3 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 CVE-2022-28346.patch create mode 100644 CVE-2022-28347.patch diff --git a/CVE-2022-28346.patch b/CVE-2022-28346.patch new file mode 100644 index 0000000..0559915 --- /dev/null +++ b/CVE-2022-28346.patch @@ -0,0 +1,168 @@ +From 2c09e68ec911919360d5f8502cefc312f9e03c5d Mon Sep 17 00:00:00 2001 +From: Mariusz Felisiak +Date: Fri, 1 Apr 2022 08:10:22 +0200 +Subject: [PATCH] [2.2.x] 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. + +Backport of 93cae5cb2f9a4ef1514cf1a41f714fef08005200 from main. +--- + django/db/models/sql/query.py | 14 ++++++++++ + docs/releases/2.2.28.txt | 8 ++++++ + tests/aggregation/tests.py | 9 ++++++ + tests/annotations/tests.py | 34 +++++++++++++++++++++++ + tests/expressions/test_queryset_values.py | 9 ++++++ + tests/queries/tests.py | 9 ++++++ + 6 files changed, 83 insertions(+) + +diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py +index b99f0e90efad..412e817f107e 100644 +--- a/django/db/models/sql/query.py ++++ b/django/db/models/sql/query.py +@@ -8,6 +8,7 @@ + """ + import difflib + import functools ++import re + from collections import Counter, OrderedDict, namedtuple + from collections.abc import Iterator, Mapping + from itertools import chain, count, product +@@ -40,6 +41,10 @@ + + __all__ = ['Query', 'RawQuery'] + ++# Quotation marks ('"`[]), whitespace characters, semicolons, or inline ++# SQL comments are forbidden in column aliases. ++FORBIDDEN_ALIAS_PATTERN = re.compile(r"['`\"\]\[;\s]|--|/\*|\*/") ++ + + def get_field_names_from_opts(opts): + return set(chain.from_iterable( +@@ -994,8 +999,16 @@ def join_parent_model(self, opts, model, alias, seen): + alias = seen[int_model] = join_info.joins[-1] + 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): + """Add a single annotation expression to the Query.""" ++ self.check_alias(alias) + annotation = annotation.resolve_expression(self, allow_joins=True, reuse=None, + summarize=is_summary) + self.append_annotation_mask([alias]) +@@ -1873,6 +1886,7 @@ def add_extra(self, select, select_params, where, params, tables, order_by): + else: + param_iter = iter([]) + for name, entry in select.items(): ++ self.check_alias(name) + entry = str(entry) + entry_params = [] + pos = entry.find("%s") +diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py +index 3820496c9fd5..501a18700baf 100644 +--- a/tests/aggregation/tests.py ++++ b/tests/aggregation/tests.py +@@ -1114,3 +1114,12 @@ def test_arguments_must_be_expressions(self): + Book.objects.aggregate(is_book=True) + with self.assertRaisesMessage(TypeError, msg % ', '.join([str(FloatField()), 'True'])): + Book.objects.aggregate(FloatField(), Avg('price'), is_book=True) ++ ++ 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")}) +diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py +index 021f59d2d71d..27cd7ebfb826 100644 +--- a/tests/annotations/tests.py ++++ b/tests/annotations/tests.py +@@ -598,3 +598,37 @@ def test_annotation_filter_with_subquery(self): + total_books=Subquery(long_books_qs, output_field=IntegerField()), + ).values('name') + self.assertCountEqual(publisher_books_qs, [{'name': 'Sams'}, {'name': 'Morgan Kaufmann'}]) ++ ++ 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)}) +diff --git a/tests/expressions/test_queryset_values.py b/tests/expressions/test_queryset_values.py +index e26459796807..0804531869d9 100644 +--- a/tests/expressions/test_queryset_values.py ++++ b/tests/expressions/test_queryset_values.py +@@ -27,6 +27,15 @@ def test_values_expression(self): + [{'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): + # values() applies annotate() first, so values selected are grouped by + # id, not firstname. +diff --git a/tests/queries/tests.py b/tests/queries/tests.py +index e72ecaa654c8..99ab57f4fc2e 100644 +--- a/tests/queries/tests.py ++++ b/tests/queries/tests.py +@@ -1737,6 +1737,15 @@ def test_extra_select_literal_percent_s(self): + '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"}) ++ + + class SelectRelatedTests(TestCase): + def test_tickets_3045_3288(self): diff --git a/CVE-2022-28347.patch b/CVE-2022-28347.patch new file mode 100644 index 0000000..c7b106c --- /dev/null +++ b/CVE-2022-28347.patch @@ -0,0 +1,155 @@ +From 29a6c98b4c13af82064f993f0acc6e8fafa4d3f5 Mon Sep 17 00:00:00 2001 +From: Mariusz Felisiak +Date: Fri, 1 Apr 2022 13:48:47 +0200 +Subject: [PATCH] [2.2.x] Fixed CVE-2022-28347 -- Protected + QuerySet.explain(**options) against SQL injection on PostgreSQL. + +Backport of 6723a26e59b0b5429a0c5873941e01a2e1bdbb81 from main. +--- + django/db/backends/postgresql/features.py | 1 - + django/db/backends/postgresql/operations.py | 27 +++++++++++++---- + django/db/models/sql/query.py | 10 +++++++ + docs/releases/2.2.28.txt | 7 +++++ + tests/queries/test_explain.py | 33 +++++++++++++++++++-- + 5 files changed, 70 insertions(+), 8 deletions(-) + +diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py +index 5c8701c396d4..9f63ca6b0ce1 100644 +--- a/django/db/backends/postgresql/features.py ++++ b/django/db/backends/postgresql/features.py +@@ -53,7 +53,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): + supports_over_clause = True + supports_aggregate_filter_clause = True + supported_explain_formats = {'JSON', 'TEXT', 'XML', 'YAML'} +- validates_explain_options = False # A query will error on invalid options. + + @cached_property + def is_postgresql_9_5(self): +diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py +index 66e5482be6ba..66ac2d5d108c 100644 +--- a/django/db/backends/postgresql/operations.py ++++ b/django/db/backends/postgresql/operations.py +@@ -8,6 +8,18 @@ + class DatabaseOperations(BaseDatabaseOperations): + cast_char_field_without_max_length = 'varchar' + explain_prefix = 'EXPLAIN' ++ explain_options = frozenset( ++ [ ++ "ANALYZE", ++ "BUFFERS", ++ "COSTS", ++ "SETTINGS", ++ "SUMMARY", ++ "TIMING", ++ "VERBOSE", ++ "WAL", ++ ] ++ ) + cast_data_types = { + 'AutoField': 'integer', + 'BigAutoField': 'bigint', +@@ -267,15 +279,20 @@ def window_frame_range_start_end(self, start=None, end=None): + return start_, end_ + + def explain_query_prefix(self, format=None, **options): +- prefix = super().explain_query_prefix(format) + extra = {} +- if format: +- extra['FORMAT'] = format ++ # Normalize options. + if options: +- extra.update({ ++ options = { + name.upper(): 'true' if value else 'false' + for name, value in options.items() +- }) ++ } ++ for valid_option in self.explain_options: ++ value = options.pop(valid_option, None) ++ if value is not None: ++ extra[valid_option.upper()] = value ++ prefix = super().explain_query_prefix(format, **options) ++ if format: ++ extra['FORMAT'] = format + if extra: + prefix += ' (%s)' % ', '.join('%s %s' % i for i in extra.items()) + return prefix +diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py +index 412e817f107e..1e823cfe74b1 100644 +--- a/django/db/models/sql/query.py ++++ b/django/db/models/sql/query.py +@@ -45,6 +45,10 @@ + # SQL comments are forbidden in column aliases. + FORBIDDEN_ALIAS_PATTERN = re.compile(r"['`\"\]\[;\s]|--|/\*|\*/") + ++# Inspired from ++# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS ++EXPLAIN_OPTIONS_PATTERN = re.compile(r"[\w\-]+") ++ + + def get_field_names_from_opts(opts): + return set(chain.from_iterable( +@@ -528,6 +532,12 @@ def has_results(self, using): + + def explain(self, using, format=None, **options): + q = self.clone() ++ for option_name in options: ++ if ( ++ not EXPLAIN_OPTIONS_PATTERN.fullmatch(option_name) or ++ "--" in option_name ++ ): ++ raise ValueError("Invalid option name: '%s'." % option_name) + q.explain_query = True + q.explain_format = format + q.explain_options = options +diff --git a/tests/queries/test_explain.py b/tests/queries/test_explain.py +index 9428bd88e9c3..209c1923071e 100644 +--- a/tests/queries/test_explain.py ++++ b/tests/queries/test_explain.py +@@ -41,8 +41,8 @@ def test_basic(self): + + @skipUnlessDBFeature('validates_explain_options') + def test_unknown_options(self): +- with self.assertRaisesMessage(ValueError, 'Unknown options: test, test2'): +- Tag.objects.all().explain(test=1, test2=1) ++ with self.assertRaisesMessage(ValueError, "Unknown options: TEST, TEST2"): ++ Tag.objects.all().explain(**{"TEST": 1, "TEST2": 1}) + + def test_unknown_format(self): + msg = 'DOES NOT EXIST is not a recognized format.' +@@ -71,6 +71,35 @@ def test_postgres_options(self): + option = '{} {}'.format(name.upper(), 'true' if value else 'false') + self.assertIn(option, captured_queries[0]['sql']) + ++ def test_option_sql_injection(self): ++ qs = Tag.objects.filter(name="test") ++ options = {"SUMMARY true) SELECT 1; --": True} ++ msg = "Invalid option name: 'SUMMARY true) SELECT 1; --'" ++ with self.assertRaisesMessage(ValueError, msg): ++ qs.explain(**options) ++ ++ def test_invalid_option_names(self): ++ qs = Tag.objects.filter(name="test") ++ tests = [ ++ 'opt"ion', ++ "o'ption", ++ "op`tion", ++ "opti on", ++ "option--", ++ "optio\tn", ++ "o\nption", ++ "option;", ++ "你 好", ++ # [] are used by MSSQL. ++ "option[", ++ "option]", ++ ] ++ for invalid_option in tests: ++ with self.subTest(invalid_option): ++ msg = "Invalid option name: '%s'" % invalid_option ++ with self.assertRaisesMessage(ValueError, msg): ++ qs.explain(**{invalid_option: True}) ++ + @unittest.skipUnless(connection.vendor == 'mysql', 'MySQL specific') + def test_mysql_text_to_traditional(self): + # Initialize the cached property, if needed, to prevent a query for diff --git a/python-django.spec b/python-django.spec index 4b783f7..35636fb 100644 --- a/python-django.spec +++ b/python-django.spec @@ -1,11 +1,17 @@ %global _empty_manifest_terminate_build 0 Name: python-django Version: 2.2.27 -Release: 1 +Release: 2 Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design. License: Apache-2.0 and Python-2.0 and OFL-1.1 and MIT URL: https://www.djangoproject.com/ Source0: https://github.com/django/django/archive/refs/tags/2.2.27.tar.gz + +#https://github.com/django/django/commit/2c09e68ec911919360d5f8502cefc312f9e03c5d +Patch0: CVE-2022-28346.patch +#https://github.com/django/django/commit/29a6c98b4c13af82064f993f0acc6e8fafa4d3f5 +Patch1: CVE-2022-28347.patch + BuildArch: noarch %description A high-level Python Web framework that encourages rapid development and clean, pragmatic design. @@ -31,7 +37,7 @@ Provides: python3-Django-doc Development documents and examples for Django %prep -%autosetup -n django-2.2.27 +%autosetup -n django-%{version} -p1 %build %py3_build @@ -71,6 +77,9 @@ mv %{buildroot}/doclist.lst . %{_docdir}/* %changelog +* Thu Apr 21 2022 yaoxin - 2.2.27-2 +- Fix CVE-2022-28346 CVE-2022-28347 + * Thu Feb 10 2022 houyingchao - 2.2.27-1 - Upgrade to 2.2.27 - Fix CVE-2021-45115 CVE-2021-45116 CVE-2021-45452 CVE-2022-22818 CVE-2022-23833 -- Gitee