Skip to content

Commit 2a8ec7f

Browse files
apollo13adamchainz
authored andcommitted
[4.0.x] Fixed CVE-2021-45116 -- Fixed potential information disclosure in dictsort template filter.
Thanks to Dennis Brinkrolf for the report. Co-authored-by: Adam Johnson <me@adamj.eu>
1 parent df79ef0 commit 2a8ec7f

File tree

7 files changed

+135
-7
lines changed

7 files changed

+135
-7
lines changed

django/template/defaultfilters.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from django.utils.timesince import timesince, timeuntil
2323
from django.utils.translation import gettext, ngettext
2424

25-
from .base import Variable, VariableDoesNotExist
25+
from .base import VARIABLE_ATTRIBUTE_SEPARATOR
2626
from .library import Library
2727

2828
register = Library()
@@ -503,7 +503,7 @@ def striptags(value):
503503
def _property_resolver(arg):
504504
"""
505505
When arg is convertible to float, behave like operator.itemgetter(arg)
506-
Otherwise, behave like Variable(arg).resolve
506+
Otherwise, chain __getitem__() and getattr().
507507
508508
>>> _property_resolver(1)('abc')
509509
'b'
@@ -521,7 +521,19 @@ def _property_resolver(arg):
521521
try:
522522
float(arg)
523523
except ValueError:
524-
return Variable(arg).resolve
524+
if VARIABLE_ATTRIBUTE_SEPARATOR + '_' in arg or arg[0] == '_':
525+
raise AttributeError('Access to private variables is forbidden.')
526+
parts = arg.split(VARIABLE_ATTRIBUTE_SEPARATOR)
527+
528+
def resolve(value):
529+
for part in parts:
530+
try:
531+
value = value[part]
532+
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
533+
value = getattr(value, part)
534+
return value
535+
536+
return resolve
525537
else:
526538
return itemgetter(arg)
527539

@@ -534,7 +546,7 @@ def dictsort(value, arg):
534546
"""
535547
try:
536548
return sorted(value, key=_property_resolver(arg))
537-
except (TypeError, VariableDoesNotExist):
549+
except (AttributeError, TypeError):
538550
return ''
539551

540552

@@ -546,7 +558,7 @@ def dictsortreversed(value, arg):
546558
"""
547559
try:
548560
return sorted(value, key=_property_resolver(arg), reverse=True)
549-
except (TypeError, VariableDoesNotExist):
561+
except (AttributeError, TypeError):
550562
return ''
551563

552564

docs/ref/templates/builtins.txt

+7
Original file line numberDiff line numberDiff line change
@@ -1577,6 +1577,13 @@ produce empty output::
15771577

15781578
{{ values|dictsort:"0" }}
15791579

1580+
Ordering by elements at specified index is not supported on dictionaries.
1581+
1582+
.. versionchanged:: 2.2.26
1583+
1584+
In older versions, ordering elements at specified index was supported on
1585+
dictionaries.
1586+
15801587
.. templatefilter:: dictsortreversed
15811588

15821589
``dictsortreversed``

docs/releases/2.2.26.txt

+16
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,19 @@ In order to mitigate this issue, relatively long values are now ignored by
2020

2121
This issue has severity "medium" according to the :ref:`Django security policy
2222
<security-disclosure>`.
23+
24+
CVE-2021-45116: Potential information disclosure in ``dictsort`` template filter
25+
================================================================================
26+
27+
Due to leveraging the Django Template Language's variable resolution logic, the
28+
:tfilter:`dictsort` template filter was potentially vulnerable to information
29+
disclosure or unintended method calls, if passed a suitably crafted key.
30+
31+
In order to avoid this possibility, ``dictsort`` now works with a restricted
32+
resolution logic, that will not call methods, nor allow indexing on
33+
dictionaries.
34+
35+
As a reminder, all untrusted user input should be validated before use.
36+
37+
This issue has severity "low" according to the :ref:`Django security policy
38+
<security-disclosure>`.

docs/releases/3.2.11.txt

+16
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,19 @@ In order to mitigate this issue, relatively long values are now ignored by
2020

2121
This issue has severity "medium" according to the :ref:`Django security policy
2222
<security-disclosure>`.
23+
24+
CVE-2021-45116: Potential information disclosure in ``dictsort`` template filter
25+
================================================================================
26+
27+
Due to leveraging the Django Template Language's variable resolution logic, the
28+
:tfilter:`dictsort` template filter was potentially vulnerable to information
29+
disclosure or unintended method calls, if passed a suitably crafted key.
30+
31+
In order to avoid this possibility, ``dictsort`` now works with a restricted
32+
resolution logic, that will not call methods, nor allow indexing on
33+
dictionaries.
34+
35+
As a reminder, all untrusted user input should be validated before use.
36+
37+
This issue has severity "low" according to the :ref:`Django security policy
38+
<security-disclosure>`.

docs/releases/4.0.1.txt

+16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ In order to mitigate this issue, relatively long values are now ignored by
2121
This issue has severity "medium" according to the :ref:`Django security policy
2222
<security-disclosure>`.
2323

24+
CVE-2021-45116: Potential information disclosure in ``dictsort`` template filter
25+
================================================================================
26+
27+
Due to leveraging the Django Template Language's variable resolution logic, the
28+
:tfilter:`dictsort` template filter was potentially vulnerable to information
29+
disclosure or unintended method calls, if passed a suitably crafted key.
30+
31+
In order to avoid this possibility, ``dictsort`` now works with a restricted
32+
resolution logic, that will not call methods, nor allow indexing on
33+
dictionaries.
34+
35+
As a reminder, all untrusted user input should be validated before use.
36+
37+
This issue has severity "low" according to the :ref:`Django security policy
38+
<security-disclosure>`.
39+
2440
Bugfixes
2541
========
2642

tests/template_tests/filter_tests/test_dictsort.py

+57-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,58 @@
1-
from django.template.defaultfilters import dictsort
1+
from django.template.defaultfilters import _property_resolver, dictsort
22
from django.test import SimpleTestCase
33

44

5+
class User:
6+
password = 'abc'
7+
8+
_private = 'private'
9+
10+
@property
11+
def test_property(self):
12+
return 'cde'
13+
14+
def test_method(self):
15+
"""This is just a test method."""
16+
17+
518
class FunctionTests(SimpleTestCase):
619

20+
def test_property_resolver(self):
21+
user = User()
22+
dict_data = {'a': {
23+
'b1': {'c': 'result1'},
24+
'b2': user,
25+
'b3': {'0': 'result2'},
26+
'b4': [0, 1, 2],
27+
}}
28+
list_data = ['a', 'b', 'c']
29+
tests = [
30+
('a.b1.c', dict_data, 'result1'),
31+
('a.b2.password', dict_data, 'abc'),
32+
('a.b2.test_property', dict_data, 'cde'),
33+
# The method should not get called.
34+
('a.b2.test_method', dict_data, user.test_method),
35+
('a.b3.0', dict_data, 'result2'),
36+
(0, list_data, 'a'),
37+
]
38+
for arg, data, expected_value in tests:
39+
with self.subTest(arg=arg):
40+
self.assertEqual(_property_resolver(arg)(data), expected_value)
41+
# Invalid lookups.
42+
fail_tests = [
43+
('a.b1.d', dict_data, AttributeError),
44+
('a.b2.password.0', dict_data, AttributeError),
45+
('a.b2._private', dict_data, AttributeError),
46+
('a.b4.0', dict_data, AttributeError),
47+
('a', list_data, AttributeError),
48+
('0', list_data, TypeError),
49+
(4, list_data, IndexError),
50+
]
51+
for arg, data, expected_exception in fail_tests:
52+
with self.subTest(arg=arg):
53+
with self.assertRaises(expected_exception):
54+
_property_resolver(arg)(data)
55+
756
def test_sort(self):
857
sorted_dicts = dictsort(
958
[{'age': 23, 'name': 'Barbara-Ann'},
@@ -21,7 +70,7 @@ def test_sort(self):
2170

2271
def test_dictsort_complex_sorting_key(self):
2372
"""
24-
Since dictsort uses template.Variable under the hood, it can sort
73+
Since dictsort uses dict.get()/getattr() under the hood, it can sort
2574
on keys like 'foo.bar'.
2675
"""
2776
data = [
@@ -60,3 +109,9 @@ def test_invalid_values(self):
60109
self.assertEqual(dictsort('Hello!', 'age'), '')
61110
self.assertEqual(dictsort({'a': 1}, 'age'), '')
62111
self.assertEqual(dictsort(1, 'age'), '')
112+
113+
def test_invalid_args(self):
114+
"""Fail silently if invalid lookups are passed."""
115+
self.assertEqual(dictsort([{}], '._private'), '')
116+
self.assertEqual(dictsort([{'_private': 'test'}], '_private'), '')
117+
self.assertEqual(dictsort([{'nested': {'_private': 'test'}}], 'nested._private'), '')

tests/template_tests/filter_tests/test_dictsortreversed.py

+6
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,9 @@ def test_invalid_values(self):
4646
self.assertEqual(dictsortreversed('Hello!', 'age'), '')
4747
self.assertEqual(dictsortreversed({'a': 1}, 'age'), '')
4848
self.assertEqual(dictsortreversed(1, 'age'), '')
49+
50+
def test_invalid_args(self):
51+
"""Fail silently if invalid lookups are passed."""
52+
self.assertEqual(dictsortreversed([{}], '._private'), '')
53+
self.assertEqual(dictsortreversed([{'_private': 'test'}], '_private'), '')
54+
self.assertEqual(dictsortreversed([{'nested': {'_private': 'test'}}], 'nested._private'), '')

0 commit comments

Comments
 (0)