1 |
From c7fe895bca06daf12cc1670b56eaf72a1ef27a16 Mon Sep 17 00:00:00 2001 |
2 |
From: Florian Apolloner <florian@apolloner.eu> |
3 |
Date: Mon, 27 Dec 2021 14:53:18 +0100 |
4 |
Subject: [PATCH] [3.2.x] Fixed CVE-2021-45116 -- Fixed potential information |
5 |
disclosure in dictsort template filter. |
6 |
|
7 |
Thanks to Dennis Brinkrolf for the report. |
8 |
|
9 |
Co-authored-by: Adam Johnson <me@adamj.eu> |
10 |
--- |
11 |
django/template/defaultfilters.py | 22 +++++-- |
12 |
docs/ref/templates/builtins.txt | 7 +++ |
13 |
docs/releases/2.2.26.txt | 16 +++++ |
14 |
docs/releases/3.2.11.txt | 16 +++++ |
15 |
.../filter_tests/test_dictsort.py | 59 ++++++++++++++++++- |
16 |
.../filter_tests/test_dictsortreversed.py | 6 ++ |
17 |
6 files changed, 119 insertions(+), 7 deletions(-) |
18 |
|
19 |
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py |
20 |
index 1c844580c651..92050122abdf 100644 |
21 |
--- a/django/template/defaultfilters.py |
22 |
+++ b/django/template/defaultfilters.py |
23 |
@@ -22,7 +22,7 @@ |
24 |
from django.utils.timesince import timesince, timeuntil |
25 |
from django.utils.translation import gettext, ngettext |
26 |
|
27 |
-from .base import Variable, VariableDoesNotExist |
28 |
+from .base import VARIABLE_ATTRIBUTE_SEPARATOR |
29 |
from .library import Library |
30 |
|
31 |
register = Library() |
32 |
@@ -481,7 +481,7 @@ def striptags(value): |
33 |
def _property_resolver(arg): |
34 |
""" |
35 |
When arg is convertible to float, behave like operator.itemgetter(arg) |
36 |
- Otherwise, behave like Variable(arg).resolve |
37 |
+ Otherwise, chain __getitem__() and getattr(). |
38 |
|
39 |
>>> _property_resolver(1)('abc') |
40 |
'b' |
41 |
@@ -499,7 +499,19 @@ def _property_resolver(arg): |
42 |
try: |
43 |
float(arg) |
44 |
except ValueError: |
45 |
- return Variable(arg).resolve |
46 |
+ if VARIABLE_ATTRIBUTE_SEPARATOR + '_' in arg or arg[0] == '_': |
47 |
+ raise AttributeError('Access to private variables is forbidden.') |
48 |
+ parts = arg.split(VARIABLE_ATTRIBUTE_SEPARATOR) |
49 |
+ |
50 |
+ def resolve(value): |
51 |
+ for part in parts: |
52 |
+ try: |
53 |
+ value = value[part] |
54 |
+ except (AttributeError, IndexError, KeyError, TypeError, ValueError): |
55 |
+ value = getattr(value, part) |
56 |
+ return value |
57 |
+ |
58 |
+ return resolve |
59 |
else: |
60 |
return itemgetter(arg) |
61 |
|
62 |
@@ -512,7 +524,7 @@ def dictsort(value, arg): |
63 |
""" |
64 |
try: |
65 |
return sorted(value, key=_property_resolver(arg)) |
66 |
- except (TypeError, VariableDoesNotExist): |
67 |
+ except (AttributeError, TypeError): |
68 |
return '' |
69 |
|
70 |
|
71 |
@@ -524,7 +536,7 @@ def dictsortreversed(value, arg): |
72 |
""" |
73 |
try: |
74 |
return sorted(value, key=_property_resolver(arg), reverse=True) |
75 |
- except (TypeError, VariableDoesNotExist): |
76 |
+ except (AttributeError, TypeError): |
77 |
return '' |
78 |
|
79 |
|
80 |
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt |
81 |
index 709f23172930..ee7cc0eda3dd 100644 |
82 |
--- a/docs/ref/templates/builtins.txt |
83 |
+++ b/docs/ref/templates/builtins.txt |
84 |
@@ -1586,6 +1586,13 @@ produce empty output:: |
85 |
|
86 |
{{ values|dictsort:"0" }} |
87 |
|
88 |
+Ordering by elements at specified index is not supported on dictionaries. |
89 |
+ |
90 |
+.. versionchanged:: 2.2.26 |
91 |
+ |
92 |
+ In older versions, ordering elements at specified index was supported on |
93 |
+ dictionaries. |
94 |
+ |
95 |
.. templatefilter:: dictsortreversed |
96 |
|
97 |
``dictsortreversed`` |
98 |
diff --git a/tests/template_tests/filter_tests/test_dictsort.py b/tests/template_tests/filter_tests/test_dictsort.py |
99 |
index 00c2bd42cbd8..3de247fd86fa 100644 |
100 |
--- a/tests/template_tests/filter_tests/test_dictsort.py |
101 |
+++ b/tests/template_tests/filter_tests/test_dictsort.py |
102 |
@@ -1,9 +1,58 @@ |
103 |
-from django.template.defaultfilters import dictsort |
104 |
+from django.template.defaultfilters import _property_resolver, dictsort |
105 |
from django.test import SimpleTestCase |
106 |
|
107 |
|
108 |
+class User: |
109 |
+ password = 'abc' |
110 |
+ |
111 |
+ _private = 'private' |
112 |
+ |
113 |
+ @property |
114 |
+ def test_property(self): |
115 |
+ return 'cde' |
116 |
+ |
117 |
+ def test_method(self): |
118 |
+ """This is just a test method.""" |
119 |
+ |
120 |
+ |
121 |
class FunctionTests(SimpleTestCase): |
122 |
|
123 |
+ def test_property_resolver(self): |
124 |
+ user = User() |
125 |
+ dict_data = {'a': { |
126 |
+ 'b1': {'c': 'result1'}, |
127 |
+ 'b2': user, |
128 |
+ 'b3': {'0': 'result2'}, |
129 |
+ 'b4': [0, 1, 2], |
130 |
+ }} |
131 |
+ list_data = ['a', 'b', 'c'] |
132 |
+ tests = [ |
133 |
+ ('a.b1.c', dict_data, 'result1'), |
134 |
+ ('a.b2.password', dict_data, 'abc'), |
135 |
+ ('a.b2.test_property', dict_data, 'cde'), |
136 |
+ # The method should not get called. |
137 |
+ ('a.b2.test_method', dict_data, user.test_method), |
138 |
+ ('a.b3.0', dict_data, 'result2'), |
139 |
+ (0, list_data, 'a'), |
140 |
+ ] |
141 |
+ for arg, data, expected_value in tests: |
142 |
+ with self.subTest(arg=arg): |
143 |
+ self.assertEqual(_property_resolver(arg)(data), expected_value) |
144 |
+ # Invalid lookups. |
145 |
+ fail_tests = [ |
146 |
+ ('a.b1.d', dict_data, AttributeError), |
147 |
+ ('a.b2.password.0', dict_data, AttributeError), |
148 |
+ ('a.b2._private', dict_data, AttributeError), |
149 |
+ ('a.b4.0', dict_data, AttributeError), |
150 |
+ ('a', list_data, AttributeError), |
151 |
+ ('0', list_data, TypeError), |
152 |
+ (4, list_data, IndexError), |
153 |
+ ] |
154 |
+ for arg, data, expected_exception in fail_tests: |
155 |
+ with self.subTest(arg=arg): |
156 |
+ with self.assertRaises(expected_exception): |
157 |
+ _property_resolver(arg)(data) |
158 |
+ |
159 |
def test_sort(self): |
160 |
sorted_dicts = dictsort( |
161 |
[{'age': 23, 'name': 'Barbara-Ann'}, |
162 |
@@ -21,7 +70,7 @@ def test_sort(self): |
163 |
|
164 |
def test_dictsort_complex_sorting_key(self): |
165 |
""" |
166 |
- Since dictsort uses template.Variable under the hood, it can sort |
167 |
+ Since dictsort uses dict.get()/getattr() under the hood, it can sort |
168 |
on keys like 'foo.bar'. |
169 |
""" |
170 |
data = [ |
171 |
@@ -60,3 +109,9 @@ def test_invalid_values(self): |
172 |
self.assertEqual(dictsort('Hello!', 'age'), '') |
173 |
self.assertEqual(dictsort({'a': 1}, 'age'), '') |
174 |
self.assertEqual(dictsort(1, 'age'), '') |
175 |
+ |
176 |
+ def test_invalid_args(self): |
177 |
+ """Fail silently if invalid lookups are passed.""" |
178 |
+ self.assertEqual(dictsort([{}], '._private'), '') |
179 |
+ self.assertEqual(dictsort([{'_private': 'test'}], '_private'), '') |
180 |
+ self.assertEqual(dictsort([{'nested': {'_private': 'test'}}], 'nested._private'), '') |
181 |
diff --git a/tests/template_tests/filter_tests/test_dictsortreversed.py b/tests/template_tests/filter_tests/test_dictsortreversed.py |
182 |
index ada199e127d2..e2e24e312849 100644 |
183 |
--- a/tests/template_tests/filter_tests/test_dictsortreversed.py |
184 |
+++ b/tests/template_tests/filter_tests/test_dictsortreversed.py |
185 |
@@ -46,3 +46,9 @@ def test_invalid_values(self): |
186 |
self.assertEqual(dictsortreversed('Hello!', 'age'), '') |
187 |
self.assertEqual(dictsortreversed({'a': 1}, 'age'), '') |
188 |
self.assertEqual(dictsortreversed(1, 'age'), '') |
189 |
+ |
190 |
+ def test_invalid_args(self): |
191 |
+ """Fail silently if invalid lookups are passed.""" |
192 |
+ self.assertEqual(dictsortreversed([{}], '._private'), '') |
193 |
+ self.assertEqual(dictsortreversed([{'_private': 'test'}], '_private'), '') |
194 |
+ self.assertEqual(dictsortreversed([{'nested': {'_private': 'test'}}], 'nested._private'), '') |