mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
remove_values could hit the recursion limit
When operating on arbitrary return data from modules, it is possible to hit the recursion limit when cleaning out no_log values from the data. To fix this, we have to switch from recursion to iteration. Unittest for remove_values recursion limit Fixes #24560
This commit is contained in:
parent
f52dcef7ba
commit
be4e7a05fa
2 changed files with 115 additions and 20 deletions
|
@ -89,6 +89,8 @@ import pwd
|
||||||
import platform
|
import platform
|
||||||
import errno
|
import errno
|
||||||
import datetime
|
import datetime
|
||||||
|
from collections import deque
|
||||||
|
from collections import Mapping, MutableMapping, Sequence, MutableSequence, Set, MutableSet
|
||||||
from itertools import repeat, chain
|
from itertools import repeat, chain
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -113,13 +115,6 @@ except ImportError:
|
||||||
# Python2 & 3 way to get NoneType
|
# Python2 & 3 way to get NoneType
|
||||||
NoneType = type(None)
|
NoneType = type(None)
|
||||||
|
|
||||||
try:
|
|
||||||
from collections import Sequence, Mapping
|
|
||||||
except ImportError:
|
|
||||||
# python2.5
|
|
||||||
Sequence = (list, tuple)
|
|
||||||
Mapping = (dict,)
|
|
||||||
|
|
||||||
# Note: When getting Sequence from collections, it matches with strings. If
|
# Note: When getting Sequence from collections, it matches with strings. If
|
||||||
# this matters, make sure to check for strings before checking for sequencetype
|
# this matters, make sure to check for strings before checking for sequencetype
|
||||||
try:
|
try:
|
||||||
|
@ -405,20 +400,44 @@ def return_values(obj):
|
||||||
raise TypeError('Unknown parameter type: %s, %s' % (type(obj), obj))
|
raise TypeError('Unknown parameter type: %s, %s' % (type(obj), obj))
|
||||||
|
|
||||||
|
|
||||||
def remove_values(value, no_log_strings):
|
def _remove_values_conditions(value, no_log_strings, deferred_removals):
|
||||||
""" Remove strings in no_log_strings from value. If value is a container
|
"""
|
||||||
type, then remove a lot more"""
|
Helper function for :meth:`remove_values`.
|
||||||
|
|
||||||
|
:arg value: The value to check for strings that need to be stripped
|
||||||
|
:arg no_log_strings: set of strings which must be stripped out of any values
|
||||||
|
:arg deferred_removals: List which holds information about nested
|
||||||
|
containers that have to be iterated for removals. It is passed into
|
||||||
|
this function so that more entries can be added to it if value is
|
||||||
|
a container type. The format of each entry is a 2-tuple where the first
|
||||||
|
element is the ``value`` parameter and the second value is a new
|
||||||
|
container to copy the elements of ``value`` into once iterated.
|
||||||
|
:returns: if ``value`` is a scalar, returns ``value`` with two exceptions:
|
||||||
|
1. :class:`~datetime.datetime` objects which are changed into a string representation.
|
||||||
|
2. objects which are in no_log_strings are replaced with a placeholder
|
||||||
|
so that no sensitive data is leaked.
|
||||||
|
If ``value`` is a container type, returns a new empty container.
|
||||||
|
|
||||||
|
``deferred_removals`` is added to as a side-effect of this function.
|
||||||
|
|
||||||
|
.. warning:: It is up to the caller to make sure the order in which value
|
||||||
|
is passed in is correct. For instance, higher level containers need
|
||||||
|
to be passed in before lower level containers. For example, given
|
||||||
|
``{'level1': {'level2': 'level3': [True]} }`` first pass in the
|
||||||
|
dictionary for ``level1``, then the dict for ``level2``, and finally
|
||||||
|
the list for ``level3``.
|
||||||
|
"""
|
||||||
if isinstance(value, (text_type, binary_type)):
|
if isinstance(value, (text_type, binary_type)):
|
||||||
# Need native str type
|
# Need native str type
|
||||||
native_str_value = value
|
native_str_value = value
|
||||||
if isinstance(value, text_type):
|
if isinstance(value, text_type):
|
||||||
value_is_text = True
|
value_is_text = True
|
||||||
if PY2:
|
if PY2:
|
||||||
native_str_value = to_bytes(value, encoding='utf-8', errors='surrogate_or_strict')
|
native_str_value = to_bytes(value, errors='surrogate_or_strict')
|
||||||
elif isinstance(value, binary_type):
|
elif isinstance(value, binary_type):
|
||||||
value_is_text = False
|
value_is_text = False
|
||||||
if PY3:
|
if PY3:
|
||||||
native_str_value = to_text(value, encoding='utf-8', errors='surrogate_or_strict')
|
native_str_value = to_text(value, errors='surrogate_or_strict')
|
||||||
|
|
||||||
if native_str_value in no_log_strings:
|
if native_str_value in no_log_strings:
|
||||||
return 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
|
return 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
|
||||||
|
@ -431,10 +450,31 @@ def remove_values(value, no_log_strings):
|
||||||
value = to_bytes(native_str_value, encoding='utf-8', errors='surrogate_then_replace')
|
value = to_bytes(native_str_value, encoding='utf-8', errors='surrogate_then_replace')
|
||||||
else:
|
else:
|
||||||
value = native_str_value
|
value = native_str_value
|
||||||
elif isinstance(value, SEQUENCETYPE):
|
|
||||||
return [remove_values(elem, no_log_strings) for elem in value]
|
elif isinstance(value, Sequence):
|
||||||
|
if isinstance(value, MutableSequence):
|
||||||
|
new_value = type(value)()
|
||||||
|
else:
|
||||||
|
new_value = [] # Need a mutable value
|
||||||
|
deferred_removals.append((value, new_value))
|
||||||
|
value = new_value
|
||||||
|
|
||||||
|
elif isinstance(value, Set):
|
||||||
|
if isinstance(value, MutableSet):
|
||||||
|
new_value = type(value)()
|
||||||
|
else:
|
||||||
|
new_value = set() # Need a mutable value
|
||||||
|
deferred_removals.append((value, new_value))
|
||||||
|
value = new_value
|
||||||
|
|
||||||
elif isinstance(value, Mapping):
|
elif isinstance(value, Mapping):
|
||||||
return dict((k, remove_values(v, no_log_strings)) for k, v in value.items())
|
if isinstance(value, MutableMapping):
|
||||||
|
new_value = type(value)()
|
||||||
|
else:
|
||||||
|
new_value = {} # Need a mutable value
|
||||||
|
deferred_removals.append((value, new_value))
|
||||||
|
value = new_value
|
||||||
|
|
||||||
elif isinstance(value, tuple(chain(NUMBERTYPES, (bool, NoneType)))):
|
elif isinstance(value, tuple(chain(NUMBERTYPES, (bool, NoneType)))):
|
||||||
stringy_value = to_native(value, encoding='utf-8', errors='surrogate_or_strict')
|
stringy_value = to_native(value, encoding='utf-8', errors='surrogate_or_strict')
|
||||||
if stringy_value in no_log_strings:
|
if stringy_value in no_log_strings:
|
||||||
|
@ -442,13 +482,42 @@ def remove_values(value, no_log_strings):
|
||||||
for omit_me in no_log_strings:
|
for omit_me in no_log_strings:
|
||||||
if omit_me in stringy_value:
|
if omit_me in stringy_value:
|
||||||
return 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
|
return 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
|
||||||
|
|
||||||
elif isinstance(value, datetime.datetime):
|
elif isinstance(value, datetime.datetime):
|
||||||
value = value.isoformat()
|
value = value.isoformat()
|
||||||
else:
|
else:
|
||||||
raise TypeError('Value of unknown type: %s, %s' % (type(value), value))
|
raise TypeError('Value of unknown type: %s, %s' % (type(value), value))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def remove_values(value, no_log_strings):
|
||||||
|
""" Remove strings in no_log_strings from value. If value is a container
|
||||||
|
type, then remove a lot more"""
|
||||||
|
deferred_removals = deque()
|
||||||
|
|
||||||
|
no_log_strings = [to_native(s, errors='surrogate_or_strict') for s in no_log_strings]
|
||||||
|
new_value = _remove_values_conditions(value, no_log_strings, deferred_removals)
|
||||||
|
|
||||||
|
while deferred_removals:
|
||||||
|
old_data, new_data = deferred_removals.popleft()
|
||||||
|
if isinstance(new_data, Mapping):
|
||||||
|
for old_key, old_elem in old_data.items():
|
||||||
|
new_elem = _remove_values_conditions(old_elem, no_log_strings, deferred_removals)
|
||||||
|
new_data[old_key] = new_elem
|
||||||
|
else:
|
||||||
|
for elem in old_data:
|
||||||
|
new_elem = _remove_values_conditions(elem, no_log_strings, deferred_removals)
|
||||||
|
if isinstance(new_data, MutableSequence):
|
||||||
|
new_data.append(new_elem)
|
||||||
|
elif isinstance(new_data, MutableSet):
|
||||||
|
new_data.add(new_elem)
|
||||||
|
else:
|
||||||
|
raise TypeError('Unknown container type encountered when removing private values from output')
|
||||||
|
|
||||||
|
return new_value
|
||||||
|
|
||||||
|
|
||||||
def heuristic_log_sanitize(data, no_log_values=None):
|
def heuristic_log_sanitize(data, no_log_values=None):
|
||||||
''' Remove strings that look like passwords from log messages '''
|
''' Remove strings that look like passwords from log messages '''
|
||||||
# Currently filters:
|
# Currently filters:
|
||||||
|
|
|
@ -90,10 +90,9 @@ class TestRemoveValues(unittest.TestCase):
|
||||||
},
|
},
|
||||||
frozenset(['nope'])
|
frozenset(['nope'])
|
||||||
),
|
),
|
||||||
('Toshio くら', frozenset(['とみ'])),
|
(u'Toshio くら'.encode('utf-8'), frozenset([u'とみ'.encode('utf-8')])),
|
||||||
(u'Toshio くら', frozenset(['とみ'])),
|
(u'Toshio くら', frozenset([u'とみ'])),
|
||||||
)
|
)
|
||||||
|
|
||||||
dataset_remove = (
|
dataset_remove = (
|
||||||
('string', frozenset(['string']), OMIT),
|
('string', frozenset(['string']), OMIT),
|
||||||
(1234, frozenset(['1234']), OMIT),
|
(1234, frozenset(['1234']), OMIT),
|
||||||
|
@ -138,8 +137,8 @@ class TestRemoveValues(unittest.TestCase):
|
||||||
frozenset(['enigma', 'mystery', 'secret']),
|
frozenset(['enigma', 'mystery', 'secret']),
|
||||||
'This sentence has an ******** wrapped in a ******** inside of a ********. - mr ********'
|
'This sentence has an ******** wrapped in a ******** inside of a ********. - mr ********'
|
||||||
),
|
),
|
||||||
('Toshio くらとみ', frozenset(['くらとみ']), 'Toshio ********'),
|
(u'Toshio くらとみ'.encode('utf-8'), frozenset([u'くらとみ'.encode('utf-8')]), u'Toshio ********'.encode('utf-8')),
|
||||||
(u'Toshio くらとみ', frozenset(['くらとみ']), u'Toshio ********'),
|
(u'Toshio くらとみ', frozenset([u'くらとみ']), u'Toshio ********'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_no_removal(self):
|
def test_no_removal(self):
|
||||||
|
@ -152,3 +151,30 @@ class TestRemoveValues(unittest.TestCase):
|
||||||
|
|
||||||
def test_unknown_type(self):
|
def test_unknown_type(self):
|
||||||
self.assertRaises(TypeError, remove_values, object(), frozenset())
|
self.assertRaises(TypeError, remove_values, object(), frozenset())
|
||||||
|
|
||||||
|
def test_hit_recursion_limit(self):
|
||||||
|
""" Check that we do not hit a recursion limit"""
|
||||||
|
data_list = []
|
||||||
|
inner_list = data_list
|
||||||
|
for i in range(0, 10000):
|
||||||
|
new_list = []
|
||||||
|
inner_list.append(new_list)
|
||||||
|
inner_list = new_list
|
||||||
|
inner_list.append('secret')
|
||||||
|
|
||||||
|
# Check that this does not hit a recursion limit
|
||||||
|
actual_data_list = remove_values(data_list, frozenset(('secret',)))
|
||||||
|
|
||||||
|
levels = 0
|
||||||
|
inner_list = actual_data_list
|
||||||
|
while inner_list:
|
||||||
|
if isinstance(inner_list, list):
|
||||||
|
self.assertEquals(len(inner_list), 1)
|
||||||
|
else:
|
||||||
|
levels -= 1
|
||||||
|
break
|
||||||
|
inner_list = inner_list[0]
|
||||||
|
levels += 1
|
||||||
|
|
||||||
|
self.assertEquals(inner_list, self.OMIT)
|
||||||
|
self.assertEquals(levels, 10000)
|
||||||
|
|
Loading…
Reference in a new issue