Skip to content

Commit

Permalink
[3.2.x] Fixed CVE-2021-45116 -- Fixed potential information disclosur…
Browse files Browse the repository at this point in the history
…e in dictsort template filter.

Thanks to Dennis Brinkrolf for the report.

Co-authored-by: Adam Johnson <me@adamj.eu>
  • Loading branch information
2 people authored and carltongibson committed Jan 4, 2022
1 parent a8b32fe commit c7fe895
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 7 deletions.
22 changes: 17 additions & 5 deletions django/template/defaultfilters.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from django.utils.timesince import timesince, timeuntil
from django.utils.translation import gettext, ngettext

from .base import Variable, VariableDoesNotExist
from .base import VARIABLE_ATTRIBUTE_SEPARATOR
from .library import Library

register = Library()
Expand Down Expand Up @@ -481,7 +481,7 @@ def striptags(value):
def _property_resolver(arg):
"""
When arg is convertible to float, behave like operator.itemgetter(arg)
Otherwise, behave like Variable(arg).resolve
Otherwise, chain __getitem__() and getattr().
>>> _property_resolver(1)('abc')
'b'
Expand All @@ -499,7 +499,19 @@ def _property_resolver(arg):
try:
float(arg)
except ValueError:
return Variable(arg).resolve
if VARIABLE_ATTRIBUTE_SEPARATOR + '_' in arg or arg[0] == '_':
raise AttributeError('Access to private variables is forbidden.')
parts = arg.split(VARIABLE_ATTRIBUTE_SEPARATOR)

def resolve(value):
for part in parts:
try:
value = value[part]
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
value = getattr(value, part)
return value

return resolve
else:
return itemgetter(arg)

Expand All @@ -512,7 +524,7 @@ def dictsort(value, arg):
"""
try:
return sorted(value, key=_property_resolver(arg))
except (TypeError, VariableDoesNotExist):
except (AttributeError, TypeError):
return ''


Expand All @@ -524,7 +536,7 @@ def dictsortreversed(value, arg):
"""
try:
return sorted(value, key=_property_resolver(arg), reverse=True)
except (TypeError, VariableDoesNotExist):
except (AttributeError, TypeError):
return ''


Expand Down
7 changes: 7 additions & 0 deletions docs/ref/templates/builtins.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,13 @@ produce empty output::

{{ values|dictsort:"0" }}

Ordering by elements at specified index is not supported on dictionaries.

.. versionchanged:: 2.2.26

In older versions, ordering elements at specified index was supported on
dictionaries.

.. templatefilter:: dictsortreversed

``dictsortreversed``
Expand Down
16 changes: 16 additions & 0 deletions docs/releases/2.2.26.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,19 @@ In order to mitigate this issue, relatively long values are now ignored by

This issue has severity "medium" according to the :ref:`Django security policy
<security-disclosure>`.

CVE-2021-45116: Potential information disclosure in ``dictsort`` template filter
================================================================================

Due to leveraging the Django Template Language's variable resolution logic, the
:tfilter:`dictsort` template filter was potentially vulnerable to information
disclosure or unintended method calls, if passed a suitably crafted key.

In order to avoid this possibility, ``dictsort`` now works with a restricted
resolution logic, that will not call methods, nor allow indexing on
dictionaries.

As a reminder, all untrusted user input should be validated before use.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.
16 changes: 16 additions & 0 deletions docs/releases/3.2.11.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,19 @@ In order to mitigate this issue, relatively long values are now ignored by

This issue has severity "medium" according to the :ref:`Django security policy
<security-disclosure>`.

CVE-2021-45116: Potential information disclosure in ``dictsort`` template filter
================================================================================

Due to leveraging the Django Template Language's variable resolution logic, the
:tfilter:`dictsort` template filter was potentially vulnerable to information
disclosure or unintended method calls, if passed a suitably crafted key.

In order to avoid this possibility, ``dictsort`` now works with a restricted
resolution logic, that will not call methods, nor allow indexing on
dictionaries.

As a reminder, all untrusted user input should be validated before use.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.
59 changes: 57 additions & 2 deletions tests/template_tests/filter_tests/test_dictsort.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,58 @@
from django.template.defaultfilters import dictsort
from django.template.defaultfilters import _property_resolver, dictsort
from django.test import SimpleTestCase


class User:
password = 'abc'

_private = 'private'

@property
def test_property(self):
return 'cde'

def test_method(self):
"""This is just a test method."""


class FunctionTests(SimpleTestCase):

def test_property_resolver(self):
user = User()
dict_data = {'a': {
'b1': {'c': 'result1'},
'b2': user,
'b3': {'0': 'result2'},
'b4': [0, 1, 2],
}}
list_data = ['a', 'b', 'c']
tests = [
('a.b1.c', dict_data, 'result1'),
('a.b2.password', dict_data, 'abc'),
('a.b2.test_property', dict_data, 'cde'),
# The method should not get called.
('a.b2.test_method', dict_data, user.test_method),
('a.b3.0', dict_data, 'result2'),
(0, list_data, 'a'),
]
for arg, data, expected_value in tests:
with self.subTest(arg=arg):
self.assertEqual(_property_resolver(arg)(data), expected_value)
# Invalid lookups.
fail_tests = [
('a.b1.d', dict_data, AttributeError),
('a.b2.password.0', dict_data, AttributeError),
('a.b2._private', dict_data, AttributeError),
('a.b4.0', dict_data, AttributeError),
('a', list_data, AttributeError),
('0', list_data, TypeError),
(4, list_data, IndexError),
]
for arg, data, expected_exception in fail_tests:
with self.subTest(arg=arg):
with self.assertRaises(expected_exception):
_property_resolver(arg)(data)

def test_sort(self):
sorted_dicts = dictsort(
[{'age': 23, 'name': 'Barbara-Ann'},
Expand All @@ -21,7 +70,7 @@ def test_sort(self):

def test_dictsort_complex_sorting_key(self):
"""
Since dictsort uses template.Variable under the hood, it can sort
Since dictsort uses dict.get()/getattr() under the hood, it can sort
on keys like 'foo.bar'.
"""
data = [
Expand Down Expand Up @@ -60,3 +109,9 @@ def test_invalid_values(self):
self.assertEqual(dictsort('Hello!', 'age'), '')
self.assertEqual(dictsort({'a': 1}, 'age'), '')
self.assertEqual(dictsort(1, 'age'), '')

def test_invalid_args(self):
"""Fail silently if invalid lookups are passed."""
self.assertEqual(dictsort([{}], '._private'), '')
self.assertEqual(dictsort([{'_private': 'test'}], '_private'), '')
self.assertEqual(dictsort([{'nested': {'_private': 'test'}}], 'nested._private'), '')
6 changes: 6 additions & 0 deletions tests/template_tests/filter_tests/test_dictsortreversed.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,9 @@ def test_invalid_values(self):
self.assertEqual(dictsortreversed('Hello!', 'age'), '')
self.assertEqual(dictsortreversed({'a': 1}, 'age'), '')
self.assertEqual(dictsortreversed(1, 'age'), '')

def test_invalid_args(self):
"""Fail silently if invalid lookups are passed."""
self.assertEqual(dictsortreversed([{}], '._private'), '')
self.assertEqual(dictsortreversed([{'_private': 'test'}], '_private'), '')
self.assertEqual(dictsortreversed([{'nested': {'_private': 'test'}}], 'nested._private'), '')

0 comments on commit c7fe895

Please sign in to comment.