update to version 3.2.12
This commit is contained in:
parent
7adc6e1213
commit
15a747692d
Binary file not shown.
@ -1,168 +0,0 @@
|
|||||||
From 2c09e68ec911919360d5f8502cefc312f9e03c5d Mon Sep 17 00:00:00 2001
|
|
||||||
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
|
||||||
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):
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
From 29a6c98b4c13af82064f993f0acc6e8fafa4d3f5 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
|
||||||
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
|
|
||||||
109
CVE-2022-34265.patch
Normal file
109
CVE-2022-34265.patch
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
From a9010fe5555e6086a9d9ae50069579400ef0685e Mon Sep 17 00:00:00 2001
|
||||||
|
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
||||||
|
Date: Wed, 22 Jun 2022 12:44:04 +0200
|
||||||
|
Subject: [PATCH] [3.2.x] Fixed CVE-2022-34265 -- Protected
|
||||||
|
Trunc(kind)/Extract(lookup_name) against SQL injection.
|
||||||
|
|
||||||
|
Thanks Takuto Yoshikai (Aeye Security Lab) for the report.
|
||||||
|
---
|
||||||
|
django/db/backends/base/operations.py | 3 ++
|
||||||
|
django/db/models/functions/datetime.py | 4 +++
|
||||||
|
docs/releases/3.2.14.txt | 11 ++++++
|
||||||
|
.../datetime/test_extract_trunc.py | 34 +++++++++++++++++++
|
||||||
|
4 files changed, 52 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py
|
||||||
|
index 0fcc607bcfb0..cdcd9885ba27 100644
|
||||||
|
--- a/django/db/backends/base/operations.py
|
||||||
|
+++ b/django/db/backends/base/operations.py
|
||||||
|
@@ -9,6 +9,7 @@
|
||||||
|
from django.db.backends import utils
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.encoding import force_str
|
||||||
|
+from django.utils.regex_helper import _lazy_re_compile
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDatabaseOperations:
|
||||||
|
@@ -53,6 +54,8 @@ class BaseDatabaseOperations:
|
||||||
|
# Prefix for EXPLAIN queries, or None EXPLAIN isn't supported.
|
||||||
|
explain_prefix = None
|
||||||
|
|
||||||
|
+ extract_trunc_lookup_pattern = _lazy_re_compile(r"[\w\-_()]+")
|
||||||
|
+
|
||||||
|
def __init__(self, connection):
|
||||||
|
self.connection = connection
|
||||||
|
self._cache = None
|
||||||
|
diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py
|
||||||
|
index 90e6f41be057..47651d281f19 100644
|
||||||
|
--- a/django/db/models/functions/datetime.py
|
||||||
|
+++ b/django/db/models/functions/datetime.py
|
||||||
|
@@ -41,6 +41,8 @@ def __init__(self, expression, lookup_name=None, tzinfo=None, **extra):
|
||||||
|
super().__init__(expression, **extra)
|
||||||
|
|
||||||
|
def as_sql(self, compiler, connection):
|
||||||
|
+ if not connection.ops.extract_trunc_lookup_pattern.fullmatch(self.lookup_name):
|
||||||
|
+ raise ValueError("Invalid lookup_name: %s" % self.lookup_name)
|
||||||
|
sql, params = compiler.compile(self.lhs)
|
||||||
|
lhs_output_field = self.lhs.output_field
|
||||||
|
if isinstance(lhs_output_field, DateTimeField):
|
||||||
|
@@ -192,6 +194,8 @@ def __init__(self, expression, output_field=None, tzinfo=None, is_dst=None, **ex
|
||||||
|
super().__init__(expression, output_field=output_field, **extra)
|
||||||
|
|
||||||
|
def as_sql(self, compiler, connection):
|
||||||
|
+ if not connection.ops.extract_trunc_lookup_pattern.fullmatch(self.kind):
|
||||||
|
+ raise ValueError("Invalid kind: %s" % self.kind)
|
||||||
|
inner_sql, inner_params = compiler.compile(self.lhs)
|
||||||
|
tzname = None
|
||||||
|
if isinstance(self.lhs.output_field, DateTimeField):
|
||||||
|
diff --git a/tests/db_functions/datetime/test_extract_trunc.py b/tests/db_functions/datetime/test_extract_trunc.py
|
||||||
|
index 258600127f93..27ed3ae63ee5 100644
|
||||||
|
--- a/tests/db_functions/datetime/test_extract_trunc.py
|
||||||
|
+++ b/tests/db_functions/datetime/test_extract_trunc.py
|
||||||
|
@@ -177,6 +177,23 @@ def test_extract_year_lessthan_lookup(self):
|
||||||
|
self.assertEqual(qs.count(), 1)
|
||||||
|
self.assertGreaterEqual(str(qs.query).lower().count('extract'), 2)
|
||||||
|
|
||||||
|
+ def test_extract_lookup_name_sql_injection(self):
|
||||||
|
+ start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
|
||||||
|
+ end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
|
||||||
|
+ if settings.USE_TZ:
|
||||||
|
+ start_datetime = timezone.make_aware(start_datetime)
|
||||||
|
+ end_datetime = timezone.make_aware(end_datetime)
|
||||||
|
+ self.create_model(start_datetime, end_datetime)
|
||||||
|
+ self.create_model(end_datetime, start_datetime)
|
||||||
|
+
|
||||||
|
+ msg = "Invalid lookup_name: "
|
||||||
|
+ with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
+ DTModel.objects.filter(
|
||||||
|
+ start_datetime__year=Extract(
|
||||||
|
+ "start_datetime", "day' FROM start_datetime)) OR 1=1;--"
|
||||||
|
+ )
|
||||||
|
+ ).exists()
|
||||||
|
+
|
||||||
|
def test_extract_func(self):
|
||||||
|
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
|
||||||
|
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
|
||||||
|
@@ -620,6 +637,23 @@ def test_extract_second_func(self):
|
||||||
|
)
|
||||||
|
self.assertEqual(DTModel.objects.filter(start_datetime__second=ExtractSecond('start_datetime')).count(), 2)
|
||||||
|
|
||||||
|
+ def test_trunc_lookup_name_sql_injection(self):
|
||||||
|
+ start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
|
||||||
|
+ end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
|
||||||
|
+ if settings.USE_TZ:
|
||||||
|
+ start_datetime = timezone.make_aware(start_datetime)
|
||||||
|
+ end_datetime = timezone.make_aware(end_datetime)
|
||||||
|
+ self.create_model(start_datetime, end_datetime)
|
||||||
|
+ self.create_model(end_datetime, start_datetime)
|
||||||
|
+ msg = "Invalid kind: "
|
||||||
|
+ with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
+ DTModel.objects.filter(
|
||||||
|
+ start_datetime__date=Trunc(
|
||||||
|
+ "start_datetime",
|
||||||
|
+ "year', start_datetime)) OR 1=1;--",
|
||||||
|
+ )
|
||||||
|
+ ).exists()
|
||||||
|
+
|
||||||
|
def test_trunc_func(self):
|
||||||
|
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
|
||||||
|
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
|
||||||
74
backport-CVE-2022-36359.patch
Normal file
74
backport-CVE-2022-36359.patch
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
From 8c5a1dfe34ea52cc2af21064a8654bfaa8b7a012 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Carlton Gibson <carlton.gibson@noumenal.es>
|
||||||
|
Date: Wed, 27 Jul 2022 10:27:42 +0200
|
||||||
|
Subject: [PATCH] [3.2.x] Fixed CVE-2022-36359: Escaped filename in
|
||||||
|
Content-Disposition header.
|
||||||
|
|
||||||
|
Thanks to Motoyasu Saburi for the report.
|
||||||
|
---
|
||||||
|
django/http/response.py | 4 +++-
|
||||||
|
docs/releases/3.2.15.txt | 8 ++++++-
|
||||||
|
tests/responses/test_fileresponse.py | 35 ++++++++++++++++++++++++++++
|
||||||
|
3 files changed, 45 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/django/http/response.py b/django/http/response.py
|
||||||
|
index 1c22edaff3..73f87d7bda 100644
|
||||||
|
--- a/django/http/response.py
|
||||||
|
+++ b/django/http/response.py
|
||||||
|
@@ -485,7 +485,9 @@ class FileResponse(StreamingHttpResponse):
|
||||||
|
disposition = 'attachment' if self.as_attachment else 'inline'
|
||||||
|
try:
|
||||||
|
filename.encode('ascii')
|
||||||
|
- file_expr = 'filename="{}"'.format(filename)
|
||||||
|
+ file_expr = 'filename="{}"'.format(
|
||||||
|
+ filename.replace('\\', '\\\\').replace('"', r'\"')
|
||||||
|
+ )
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
file_expr = "filename*=utf-8''{}".format(quote(filename))
|
||||||
|
self.headers['Content-Disposition'] = '{}; {}'.format(disposition, file_expr)
|
||||||
|
diff --git a/tests/responses/test_fileresponse.py b/tests/responses/test_fileresponse.py
|
||||||
|
index 46d407bdf5..b4ef82ef3e 100644
|
||||||
|
--- a/tests/responses/test_fileresponse.py
|
||||||
|
+++ b/tests/responses/test_fileresponse.py
|
||||||
|
@@ -89,3 +89,38 @@ class FileResponseTests(SimpleTestCase):
|
||||||
|
response.headers['Content-Disposition'],
|
||||||
|
"attachment; filename*=utf-8''%E7%A5%9D%E6%82%A8%E5%B9%B3%E5%AE%89.odt"
|
||||||
|
)
|
||||||
|
+
|
||||||
|
+ def test_content_disposition_escaping(self):
|
||||||
|
+ # fmt: off
|
||||||
|
+ tests = [
|
||||||
|
+ (
|
||||||
|
+ 'multi-part-one";\" dummy".txt',
|
||||||
|
+ r"multi-part-one\";\" dummy\".txt"
|
||||||
|
+ ),
|
||||||
|
+ ]
|
||||||
|
+ # fmt: on
|
||||||
|
+ # Non-escape sequence backslashes are path segments on Windows, and are
|
||||||
|
+ # eliminated by an os.path.basename() check in FileResponse.
|
||||||
|
+ if sys.platform != "win32":
|
||||||
|
+ # fmt: off
|
||||||
|
+ tests += [
|
||||||
|
+ (
|
||||||
|
+ 'multi-part-one\\";\" dummy".txt',
|
||||||
|
+ r"multi-part-one\\\";\" dummy\".txt"
|
||||||
|
+ ),
|
||||||
|
+ (
|
||||||
|
+ 'multi-part-one\\";\\\" dummy".txt',
|
||||||
|
+ r"multi-part-one\\\";\\\" dummy\".txt"
|
||||||
|
+ )
|
||||||
|
+ ]
|
||||||
|
+ # fmt: on
|
||||||
|
+ for filename, escaped in tests:
|
||||||
|
+ with self.subTest(filename=filename, escaped=escaped):
|
||||||
|
+ response = FileResponse(
|
||||||
|
+ io.BytesIO(b"binary content"), filename=filename, as_attachment=True
|
||||||
|
+ )
|
||||||
|
+ response.close()
|
||||||
|
+ self.assertEqual(
|
||||||
|
+ response.headers["Content-Disposition"],
|
||||||
|
+ f'attachment; filename="{escaped}"',
|
||||||
|
+ )
|
||||||
|
--
|
||||||
|
2.36.1
|
||||||
|
|
||||||
@ -1,16 +1,15 @@
|
|||||||
%global _empty_manifest_terminate_build 0
|
%global _empty_manifest_terminate_build 0
|
||||||
Name: python-django
|
Name: python-django
|
||||||
Version: 2.2.27
|
Version: 3.2.12
|
||||||
Release: 2
|
Release: 1
|
||||||
Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design.
|
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
|
License: Apache-2.0 and Python-2.0 and BSD-3-Clause
|
||||||
URL: https://www.djangoproject.com/
|
URL: https://www.djangoproject.com/
|
||||||
Source0: https://github.com/django/django/archive/refs/tags/2.2.27.tar.gz
|
Source0: https://github.com/django/django/archive/refs/tags/%{version}.tar.gz
|
||||||
|
|
||||||
#https://github.com/django/django/commit/2c09e68ec911919360d5f8502cefc312f9e03c5d
|
#https://github.com/django/django/commit/a9010fe5555e6086a9d9ae50069579400ef0685e
|
||||||
Patch0: CVE-2022-28346.patch
|
Patch0: CVE-2022-34265.patch
|
||||||
#https://github.com/django/django/commit/29a6c98b4c13af82064f993f0acc6e8fafa4d3f5
|
Patch1: backport-CVE-2022-36359.patch
|
||||||
Patch1: CVE-2022-28347.patch
|
|
||||||
|
|
||||||
BuildArch: noarch
|
BuildArch: noarch
|
||||||
%description
|
%description
|
||||||
@ -77,6 +76,9 @@ mv %{buildroot}/doclist.lst .
|
|||||||
%{_docdir}/*
|
%{_docdir}/*
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Thu Oct 20 2022 Ge Wang <wangge20@h-partners.com> - 3.2.12-1
|
||||||
|
- Update to version 3.2.12 and fix CVE-2022034265, CVE-2022-36359
|
||||||
|
|
||||||
* Thu Apr 21 2022 yaoxin <yaoxin30@h-partners.com> - 2.2.27-2
|
* Thu Apr 21 2022 yaoxin <yaoxin30@h-partners.com> - 2.2.27-2
|
||||||
- Fix CVE-2022-28346 CVE-2022-28347
|
- Fix CVE-2022-28346 CVE-2022-28347
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user