CVE-2019-14858
Description
A vulnerability was found in Ansible engine 2.x up to 2.8 and Ansible tower 3.x up to 3.5. When a module has an argument_spec with sub parameters marked as no_log, passing an invalid parameter name to the module will cause the task to fail before the no_log options in the sub parameters are processed. As a result, data in the sub parameter fields will not be masked and will be displayed if Ansible is run with increased verbosity and present in the module invocation arguments for the task.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ansiblePyPI | >= 2.9.0a1, < 2.9.0rc4 | 2.9.0rc4 |
ansiblePyPI | >= 2.8.0a1, < 2.8.6 | 2.8.6 |
ansiblePyPI | >= 2.7.0a1, < 2.7.14 | 2.7.14 |
ansiblePyPI | >= 2.0, < 2.6.20 | 2.6.20 |
Affected products
1Patches
43dfb8e81bb5f[stable-2.8] Properly mask no_log values is sub parameters during failure (#63405)
8 files changed · +359 −21
changelogs/fragments/no-log-sub-options-invalid-parameter.yaml+2 −0 added@@ -0,0 +1,2 @@ +bugfixes: + - '**security issue** - properly hide parameters marked with ``no_log`` in suboptions when invalid parameters are passed to the module (CVE-2019-14858)'
lib/ansible/module_utils/basic.py+5 −2 modified@@ -1429,7 +1429,11 @@ def _handle_no_log_values(self, spec=None, param=None): if param is None: param = self.params - self.no_log_values.update(list_no_log_values(spec, param)) + try: + self.no_log_values.update(list_no_log_values(spec, param)) + except TypeError as te: + self.fail_json(msg="Failure when processing no_log parameters. Module invocation will be hidden. " + "%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'}) self._deprecations.extend(list_deprecations(spec, param)) def _check_arguments(self, check_invalid_arguments, spec=None, param=None, legal_inputs=None): @@ -1699,7 +1703,6 @@ def _handle_options(self, argument_spec=None, params=None): self._set_fallbacks(spec, param) options_aliases = self._handle_aliases(spec, param) - self._handle_no_log_values(spec, param) options_legal_inputs = list(spec.keys()) + list(options_aliases.keys()) self._check_arguments(self.check_invalid_arguments, spec, param, options_legal_inputs)
lib/ansible/module_utils/common/parameters.py+26 −0 modified@@ -8,10 +8,12 @@ from ansible.module_utils._text import to_native from ansible.module_utils.common._collections_compat import Mapping from ansible.module_utils.common.collections import is_iterable +from ansible.module_utils.common.validation import check_type_dict from ansible.module_utils.six import ( binary_type, integer_types, + string_types, text_type, ) @@ -86,6 +88,30 @@ def list_no_log_values(argument_spec, params): if no_log_object: no_log_values.update(_return_datastructure_name(no_log_object)) + # Get no_log values from suboptions + sub_argument_spec = arg_opts.get('options') + if sub_argument_spec is not None: + wanted_type = arg_opts.get('type') + sub_parameters = params.get(arg_name) + + if sub_parameters is not None: + if wanted_type == 'dict' or (wanted_type == 'list' and arg_opts.get('elements', '') == 'dict'): + # Sub parameters can be a dict or list of dicts. Ensure parameters are always a list. + if not isinstance(sub_parameters, list): + sub_parameters = [sub_parameters] + + for sub_param in sub_parameters: + # Validate dict fields in case they came in as strings + + if isinstance(sub_param, string_types): + sub_param = check_type_dict(sub_param) + + if not isinstance(sub_param, Mapping): + raise TypeError("Value '{1}' in the sub parameter field '{0}' must by a {2}, " + "not '{1.__class__.__name__}'".format(arg_name, sub_param, wanted_type)) + + no_log_values.update(list_no_log_values(sub_argument_spec, sub_param)) + return no_log_values
test/integration/targets/no_log/library/module.py+45 −0 added@@ -0,0 +1,45 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2019 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec={ + 'state': {}, + 'secret': {'no_log': True}, + 'subopt_dict': { + 'type': 'dict', + 'options': { + 'str_sub_opt1': {'no_log': True}, + 'str_sub_opt2': {}, + 'nested_subopt': { + 'type': 'dict', + 'options': { + 'n_subopt1': {'no_log': True}, + } + } + } + }, + 'subopt_list': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'subopt1': {'no_log': True}, + 'subopt2': {}, + } + } + + } + ) + module.exit_json(msg='done') + + +if __name__ == '__main__': + main()
test/integration/targets/no_log/no_log_suboptions_invalid.yml+45 −0 added@@ -0,0 +1,45 @@ +- name: test no log with suboptions + hosts: testhost + gather_facts: no + ignore_errors: yes + + tasks: + - name: Task with suboptions and invalid parameter + module: + secret: SUPREME + invalid: param + subopt_dict: + str_sub_opt1: IDIOM + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: MOCKUP + + subopt_list: + - subopt1: EDUCATED + subopt2: thridstring + - subopt1: FOOTREST + + - name: Task with suboptions as string with invalid parameter + module: + secret: FOOTREST + invalid: param + subopt_dict: str_sub_opt1=CRAFTY + + - name: Task with suboptions with dict instead of list + module: + secret: FELINE + subopt_dict: + str_sub_opt1: CRYSTAL + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: EXPECTANT + subopt_list: + foo: bar + + - name: Task with suboptions with incorrect data type + module: + secret: AGROUND + subopt_dict: 9068.21361 + subopt_list: + - subopt1: GOLIATH + - subopt1: FREEFALL
test/integration/targets/no_log/no_log_suboptions.yml+24 −0 added@@ -0,0 +1,24 @@ +- name: test no log with suboptions + hosts: testhost + gather_facts: no + + tasks: + - name: Task with suboptions + module: + secret: GLAMOROUS + subopt_dict: + str_sub_opt1: AFTERMATH + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: MANPOWER + + subopt_list: + - subopt1: UNTAPPED + subopt2: thridstring + + - subopt1: CONCERNED + + - name: Task with suboptions as string + module: + secret: MARLIN + subopt_dict: str_sub_opt1=FLICK
test/integration/targets/no_log/runme.sh+6 −0 modified@@ -13,3 +13,9 @@ set -eux # no log disabled, should produce 0 censored [ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=yes|grep -c 'output has been hidden')" = "0" ] + +# test no log for sub options +[ "$(ansible-playbook no_log_suboptions.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(MANPOWER|UNTAPPED|CONCERNED|MARLIN|FLICK)')" = "0" ] + +# test invalid data passed to a suboption +[ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(SUPREME|IDIOM|MOCKUP|EDUCATED|FOOTREST|CRAFTY|FELINE|CRYSTAL|EXPECTANT|AGROUND|GOLIATH|FREEFALL)')" = "0" ]
test/units/module_utils/common/parameters/test_list_no_log_values.py+206 −19 modified@@ -11,31 +11,218 @@ @pytest.fixture -def params(): - return { - 'secret': 'undercookwovennativity', - 'other_secret': 'cautious-slate-makeshift', - 'state': 'present', - 'value': 5, - } +def argument_spec(): + # Allow extra specs to be passed to the fixture, which will be added to the output + def _argument_spec(extra_opts=None): + spec = { + 'secret': {'type': 'str', 'no_log': True}, + 'other_secret': {'type': 'str', 'no_log': True}, + 'state': {'type': 'str'}, + 'value': {'type': 'int'}, + } + if extra_opts: + spec.update(extra_opts) -def test_list_no_log_values(params): - argument_spec = { - 'secret': {'type': 'str', 'no_log': True}, - 'other_secret': {'type': 'str', 'no_log': True}, - 'state': {'type': 'str'}, - 'value': {'type': 'int'}, - } - result = set(('undercookwovennativity', 'cautious-slate-makeshift')) - assert result == list_no_log_values(argument_spec, params) + return spec + + return _argument_spec + + +@pytest.fixture +def module_parameters(): + # Allow extra parameters to be passed to the fixture, which will be added to the output + def _module_parameters(extra_params=None): + params = { + 'secret': 'under', + 'other_secret': 'makeshift', + 'state': 'present', + 'value': 5, + } + + if extra_params: + params.update(extra_params) + + return params + + return _module_parameters -def test_list_no_log_values_no_secrets(params): +def test_list_no_log_values_no_secrets(module_parameters): argument_spec = { 'other_secret': {'type': 'str', 'no_log': False}, 'state': {'type': 'str'}, 'value': {'type': 'int'}, } - result = set() - assert result == list_no_log_values(argument_spec, params) + expected = set() + assert expected == list_no_log_values(argument_spec, module_parameters) + + +def test_list_no_log_values(argument_spec, module_parameters): + expected = set(('under', 'makeshift')) + assert expected == list_no_log_values(argument_spec(), module_parameters()) + + +@pytest.mark.parametrize('extra_params', [ + {'subopt1': 1}, + {'subopt1': 3.14159}, + {'subopt1': ['one', 'two']}, + {'subopt1': ('one', 'two')}, +]) +def test_list_no_log_values_invalid_suboptions(argument_spec, module_parameters, extra_params): + extra_opts = { + 'subopt1': { + 'type': 'dict', + 'options': { + 'sub_1_1': {}, + } + } + } + + with pytest.raises(TypeError, match=r"(Value '.*?' in the sub parameter field '.*?' must by a dict, not '.*?')" + r"|(dictionary requested, could not parse JSON or key=value)"): + list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_suboptions(argument_spec, module_parameters): + extra_opts = { + 'subopt1': { + 'type': 'dict', + 'options': { + 'sub_1_1': {'no_log': True}, + 'sub_1_2': {'type': 'list'}, + } + } + } + + extra_params = { + 'subopt1': { + 'sub_1_1': 'bagel', + 'sub_1_2': ['pebble'], + } + } + + expected = set(('under', 'makeshift', 'bagel')) + assert expected == list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_sub_suboptions(argument_spec, module_parameters): + extra_opts = { + 'sub_level_1': { + 'type': 'dict', + 'options': { + 'l1_1': {'no_log': True}, + 'l1_2': {}, + 'l1_3': { + 'type': 'dict', + 'options': { + 'l2_1': {'no_log': True}, + 'l2_2': {}, + } + } + } + } + } + + extra_params = { + 'sub_level_1': { + 'l1_1': 'saucy', + 'l1_2': 'napped', + 'l1_3': { + 'l2_1': 'corporate', + 'l2_2': 'tinsmith', + } + } + } + + expected = set(('under', 'makeshift', 'saucy', 'corporate')) + assert expected == list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_suboptions_list(argument_spec, module_parameters): + extra_opts = { + 'subopt1': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'sub_1_1': {'no_log': True}, + 'sub_1_2': {}, + } + } + } + + extra_params = { + 'subopt1': [ + { + 'sub_1_1': ['playroom', 'luxury'], + 'sub_1_2': 'deuce', + }, + { + 'sub_1_2': ['squishier', 'finished'], + } + ] + } + + expected = set(('under', 'makeshift', 'playroom', 'luxury')) + assert expected == list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_sub_suboptions_list(argument_spec, module_parameters): + extra_opts = { + 'subopt1': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'sub_1_1': {'no_log': True}, + 'sub_1_2': {}, + 'subopt2': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'sub_2_1': {'no_log': True, 'type': 'list'}, + 'sub_2_2': {}, + } + } + } + } + } + + extra_params = { + 'subopt1': { + 'sub_1_1': ['playroom', 'luxury'], + 'sub_1_2': 'deuce', + 'subopt2': [ + { + 'sub_2_1': ['basis', 'gave'], + 'sub_2_2': 'liquid', + }, + { + 'sub_2_1': ['composure', 'thumping'] + }, + ] + } + } + + expected = set(('under', 'makeshift', 'playroom', 'luxury', 'basis', 'gave', 'composure', 'thumping')) + assert expected == list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +@pytest.mark.parametrize('extra_params, expected', ( + ({'subopt_dict': 'dict_subopt1=rekindle-scandal,dict_subopt2=subgroupavenge'}, ('rekindle-scandal',)), + ({'subopt_dict': 'dict_subopt1=aversion-mutable dict_subopt2=subgroupavenge'}, ('aversion-mutable',)), + ({'subopt_dict': ['dict_subopt1=blip-marine,dict_subopt2=subgroupavenge', 'dict_subopt1=tipping,dict_subopt2=hardening']}, ('blip-marine', 'tipping')), +)) +def test_string_suboptions_as_string(argument_spec, module_parameters, extra_params, expected): + extra_opts = { + 'subopt_dict': { + 'type': 'dict', + 'options': { + 'dict_subopt1': {'no_log': True}, + 'dict_subopt2': {}, + }, + }, + } + + result = set(('under', 'makeshift')) + result.update(expected) + assert result == list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params))
f610ed3a4eb8[stable-2.9] Properly mask no_log values is sub parameters during failure (#63405)
8 files changed · +359 −21
changelogs/fragments/no-log-sub-options-invalid-parameter.yaml+2 −0 added@@ -0,0 +1,2 @@ +bugfixes: + - '**security issue** - properly hide parameters marked with ``no_log`` in suboptions when invalid parameters are passed to the module (CVE-2019-14858)'
lib/ansible/module_utils/basic.py+5 −2 modified@@ -1447,7 +1447,11 @@ def _handle_no_log_values(self, spec=None, param=None): if param is None: param = self.params - self.no_log_values.update(list_no_log_values(spec, param)) + try: + self.no_log_values.update(list_no_log_values(spec, param)) + except TypeError as te: + self.fail_json(msg="Failure when processing no_log parameters. Module invocation will be hidden. " + "%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'}) self._deprecations.extend(list_deprecations(spec, param)) def _check_arguments(self, check_invalid_arguments, spec=None, param=None, legal_inputs=None): @@ -1722,7 +1726,6 @@ def _handle_options(self, argument_spec=None, params=None, prefix=''): self._set_fallbacks(spec, param) options_aliases = self._handle_aliases(spec, param, option_prefix=new_prefix) - self._handle_no_log_values(spec, param) options_legal_inputs = list(spec.keys()) + list(options_aliases.keys()) self._check_arguments(self.check_invalid_arguments, spec, param, options_legal_inputs)
lib/ansible/module_utils/common/parameters.py+26 −0 modified@@ -8,10 +8,12 @@ from ansible.module_utils._text import to_native from ansible.module_utils.common._collections_compat import Mapping from ansible.module_utils.common.collections import is_iterable +from ansible.module_utils.common.validation import check_type_dict from ansible.module_utils.six import ( binary_type, integer_types, + string_types, text_type, ) @@ -86,6 +88,30 @@ def list_no_log_values(argument_spec, params): if no_log_object: no_log_values.update(_return_datastructure_name(no_log_object)) + # Get no_log values from suboptions + sub_argument_spec = arg_opts.get('options') + if sub_argument_spec is not None: + wanted_type = arg_opts.get('type') + sub_parameters = params.get(arg_name) + + if sub_parameters is not None: + if wanted_type == 'dict' or (wanted_type == 'list' and arg_opts.get('elements', '') == 'dict'): + # Sub parameters can be a dict or list of dicts. Ensure parameters are always a list. + if not isinstance(sub_parameters, list): + sub_parameters = [sub_parameters] + + for sub_param in sub_parameters: + # Validate dict fields in case they came in as strings + + if isinstance(sub_param, string_types): + sub_param = check_type_dict(sub_param) + + if not isinstance(sub_param, Mapping): + raise TypeError("Value '{1}' in the sub parameter field '{0}' must by a {2}, " + "not '{1.__class__.__name__}'".format(arg_name, sub_param, wanted_type)) + + no_log_values.update(list_no_log_values(sub_argument_spec, sub_param)) + return no_log_values
test/integration/targets/no_log/library/module.py+45 −0 added@@ -0,0 +1,45 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2019 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec={ + 'state': {}, + 'secret': {'no_log': True}, + 'subopt_dict': { + 'type': 'dict', + 'options': { + 'str_sub_opt1': {'no_log': True}, + 'str_sub_opt2': {}, + 'nested_subopt': { + 'type': 'dict', + 'options': { + 'n_subopt1': {'no_log': True}, + } + } + } + }, + 'subopt_list': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'subopt1': {'no_log': True}, + 'subopt2': {}, + } + } + + } + ) + module.exit_json(msg='done') + + +if __name__ == '__main__': + main()
test/integration/targets/no_log/no_log_suboptions_invalid.yml+45 −0 added@@ -0,0 +1,45 @@ +- name: test no log with suboptions + hosts: testhost + gather_facts: no + ignore_errors: yes + + tasks: + - name: Task with suboptions and invalid parameter + module: + secret: SUPREME + invalid: param + subopt_dict: + str_sub_opt1: IDIOM + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: MOCKUP + + subopt_list: + - subopt1: EDUCATED + subopt2: thridstring + - subopt1: FOOTREST + + - name: Task with suboptions as string with invalid parameter + module: + secret: FOOTREST + invalid: param + subopt_dict: str_sub_opt1=CRAFTY + + - name: Task with suboptions with dict instead of list + module: + secret: FELINE + subopt_dict: + str_sub_opt1: CRYSTAL + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: EXPECTANT + subopt_list: + foo: bar + + - name: Task with suboptions with incorrect data type + module: + secret: AGROUND + subopt_dict: 9068.21361 + subopt_list: + - subopt1: GOLIATH + - subopt1: FREEFALL
test/integration/targets/no_log/no_log_suboptions.yml+24 −0 added@@ -0,0 +1,24 @@ +- name: test no log with suboptions + hosts: testhost + gather_facts: no + + tasks: + - name: Task with suboptions + module: + secret: GLAMOROUS + subopt_dict: + str_sub_opt1: AFTERMATH + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: MANPOWER + + subopt_list: + - subopt1: UNTAPPED + subopt2: thridstring + + - subopt1: CONCERNED + + - name: Task with suboptions as string + module: + secret: MARLIN + subopt_dict: str_sub_opt1=FLICK
test/integration/targets/no_log/runme.sh+6 −0 modified@@ -13,3 +13,9 @@ set -eux # no log disabled, should produce 0 censored [ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=yes|grep -c 'output has been hidden')" = "0" ] + +# test no log for sub options +[ "$(ansible-playbook no_log_suboptions.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(MANPOWER|UNTAPPED|CONCERNED|MARLIN|FLICK)')" = "0" ] + +# test invalid data passed to a suboption +[ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(SUPREME|IDIOM|MOCKUP|EDUCATED|FOOTREST|CRAFTY|FELINE|CRYSTAL|EXPECTANT|AGROUND|GOLIATH|FREEFALL)')" = "0" ]
test/units/module_utils/common/parameters/test_list_no_log_values.py+206 −19 modified@@ -11,31 +11,218 @@ @pytest.fixture -def params(): - return { - 'secret': 'undercookwovennativity', - 'other_secret': 'cautious-slate-makeshift', - 'state': 'present', - 'value': 5, - } +def argument_spec(): + # Allow extra specs to be passed to the fixture, which will be added to the output + def _argument_spec(extra_opts=None): + spec = { + 'secret': {'type': 'str', 'no_log': True}, + 'other_secret': {'type': 'str', 'no_log': True}, + 'state': {'type': 'str'}, + 'value': {'type': 'int'}, + } + if extra_opts: + spec.update(extra_opts) -def test_list_no_log_values(params): - argument_spec = { - 'secret': {'type': 'str', 'no_log': True}, - 'other_secret': {'type': 'str', 'no_log': True}, - 'state': {'type': 'str'}, - 'value': {'type': 'int'}, - } - result = set(('undercookwovennativity', 'cautious-slate-makeshift')) - assert result == list_no_log_values(argument_spec, params) + return spec + + return _argument_spec + + +@pytest.fixture +def module_parameters(): + # Allow extra parameters to be passed to the fixture, which will be added to the output + def _module_parameters(extra_params=None): + params = { + 'secret': 'under', + 'other_secret': 'makeshift', + 'state': 'present', + 'value': 5, + } + + if extra_params: + params.update(extra_params) + + return params + + return _module_parameters -def test_list_no_log_values_no_secrets(params): +def test_list_no_log_values_no_secrets(module_parameters): argument_spec = { 'other_secret': {'type': 'str', 'no_log': False}, 'state': {'type': 'str'}, 'value': {'type': 'int'}, } - result = set() - assert result == list_no_log_values(argument_spec, params) + expected = set() + assert expected == list_no_log_values(argument_spec, module_parameters) + + +def test_list_no_log_values(argument_spec, module_parameters): + expected = set(('under', 'makeshift')) + assert expected == list_no_log_values(argument_spec(), module_parameters()) + + +@pytest.mark.parametrize('extra_params', [ + {'subopt1': 1}, + {'subopt1': 3.14159}, + {'subopt1': ['one', 'two']}, + {'subopt1': ('one', 'two')}, +]) +def test_list_no_log_values_invalid_suboptions(argument_spec, module_parameters, extra_params): + extra_opts = { + 'subopt1': { + 'type': 'dict', + 'options': { + 'sub_1_1': {}, + } + } + } + + with pytest.raises(TypeError, match=r"(Value '.*?' in the sub parameter field '.*?' must by a dict, not '.*?')" + r"|(dictionary requested, could not parse JSON or key=value)"): + list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_suboptions(argument_spec, module_parameters): + extra_opts = { + 'subopt1': { + 'type': 'dict', + 'options': { + 'sub_1_1': {'no_log': True}, + 'sub_1_2': {'type': 'list'}, + } + } + } + + extra_params = { + 'subopt1': { + 'sub_1_1': 'bagel', + 'sub_1_2': ['pebble'], + } + } + + expected = set(('under', 'makeshift', 'bagel')) + assert expected == list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_sub_suboptions(argument_spec, module_parameters): + extra_opts = { + 'sub_level_1': { + 'type': 'dict', + 'options': { + 'l1_1': {'no_log': True}, + 'l1_2': {}, + 'l1_3': { + 'type': 'dict', + 'options': { + 'l2_1': {'no_log': True}, + 'l2_2': {}, + } + } + } + } + } + + extra_params = { + 'sub_level_1': { + 'l1_1': 'saucy', + 'l1_2': 'napped', + 'l1_3': { + 'l2_1': 'corporate', + 'l2_2': 'tinsmith', + } + } + } + + expected = set(('under', 'makeshift', 'saucy', 'corporate')) + assert expected == list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_suboptions_list(argument_spec, module_parameters): + extra_opts = { + 'subopt1': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'sub_1_1': {'no_log': True}, + 'sub_1_2': {}, + } + } + } + + extra_params = { + 'subopt1': [ + { + 'sub_1_1': ['playroom', 'luxury'], + 'sub_1_2': 'deuce', + }, + { + 'sub_1_2': ['squishier', 'finished'], + } + ] + } + + expected = set(('under', 'makeshift', 'playroom', 'luxury')) + assert expected == list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +def test_list_no_log_values_sub_suboptions_list(argument_spec, module_parameters): + extra_opts = { + 'subopt1': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'sub_1_1': {'no_log': True}, + 'sub_1_2': {}, + 'subopt2': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'sub_2_1': {'no_log': True, 'type': 'list'}, + 'sub_2_2': {}, + } + } + } + } + } + + extra_params = { + 'subopt1': { + 'sub_1_1': ['playroom', 'luxury'], + 'sub_1_2': 'deuce', + 'subopt2': [ + { + 'sub_2_1': ['basis', 'gave'], + 'sub_2_2': 'liquid', + }, + { + 'sub_2_1': ['composure', 'thumping'] + }, + ] + } + } + + expected = set(('under', 'makeshift', 'playroom', 'luxury', 'basis', 'gave', 'composure', 'thumping')) + assert expected == list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params)) + + +@pytest.mark.parametrize('extra_params, expected', ( + ({'subopt_dict': 'dict_subopt1=rekindle-scandal,dict_subopt2=subgroupavenge'}, ('rekindle-scandal',)), + ({'subopt_dict': 'dict_subopt1=aversion-mutable dict_subopt2=subgroupavenge'}, ('aversion-mutable',)), + ({'subopt_dict': ['dict_subopt1=blip-marine,dict_subopt2=subgroupavenge', 'dict_subopt1=tipping,dict_subopt2=hardening']}, ('blip-marine', 'tipping')), +)) +def test_string_suboptions_as_string(argument_spec, module_parameters, extra_params, expected): + extra_opts = { + 'subopt_dict': { + 'type': 'dict', + 'options': { + 'dict_subopt1': {'no_log': True}, + 'dict_subopt2': {}, + }, + }, + } + + result = set(('under', 'makeshift')) + result.update(expected) + assert result == list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params))
87f8d77d7047[stable-2.6] Properly mask no_log values is sub parameters during failure (#63405)
7 files changed · +151 −1
changelogs/fragments/no-log-sub-options-invalid-parameter.yaml+2 −0 added@@ -0,0 +1,2 @@ +bugfixes: + - '**security issue** - properly hide parameters marked with ``no_log`` in suboptions when invalid parameters are passed to the module (CVE-2019-14858)'
lib/ansible/module_utils/basic.py+28 −1 modified@@ -1686,6 +1686,34 @@ def _handle_no_log_values(self, spec=None, param=None): if no_log_object: self.no_log_values.update(return_values(no_log_object)) + # Get no_log values from suboptions + sub_argument_spec = arg_opts.get('options') + if sub_argument_spec is not None: + wanted_type = arg_opts.get('type') + sub_parameters = param.get(arg_name) + + if sub_parameters is not None: + if wanted_type == 'dict' or (wanted_type == 'list' and arg_opts.get('elements', '') == 'dict'): + # Sub parameters can be a dict or list of dicts. Ensure parameters are always a list. + if not isinstance(sub_parameters, list): + sub_parameters = [sub_parameters] + + for sub_param in sub_parameters: + # Validate dict fields in case they came in as strings + + if isinstance(sub_param, string_types): + sub_param = self._check_type_dict(sub_param) + + try: + if not isinstance(sub_param, Mapping): + raise TypeError("Value '{1}' in the sub parameter field '{0}' must by a {2}, " + "not '{1.__class__.__name__}'".format(arg_name, sub_param, wanted_type)) + except TypeError as te: + self.fail_json(msg="Failure when processing no_log parameters. Module invocation will be hidden. " + "%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'}) + + self._handle_no_log_values(sub_argument_spec, sub_param) + if arg_opts.get('removed_in_version') is not None and arg_name in param: self._deprecations.append({ 'msg': "Param '%s' is deprecated. See the module docs for more information" % arg_name, @@ -2053,7 +2081,6 @@ def _handle_options(self, argument_spec=None, params=None): self._set_fallbacks(spec, param) options_aliases = self._handle_aliases(spec, param) - self._handle_no_log_values(spec, param) options_legal_inputs = list(spec.keys()) + list(options_aliases.keys()) self._check_arguments(self.check_invalid_arguments, spec, param, options_legal_inputs)
test/integration/targets/no_log/library/module.py+45 −0 added@@ -0,0 +1,45 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2019 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec={ + 'state': {}, + 'secret': {'no_log': True}, + 'subopt_dict': { + 'type': 'dict', + 'options': { + 'str_sub_opt1': {'no_log': True}, + 'str_sub_opt2': {}, + 'nested_subopt': { + 'type': 'dict', + 'options': { + 'n_subopt1': {'no_log': True}, + } + } + } + }, + 'subopt_list': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'subopt1': {'no_log': True}, + 'subopt2': {}, + } + } + + } + ) + module.exit_json(msg='done') + + +if __name__ == '__main__': + main()
test/integration/targets/no_log/no_log_local.yml+1 −0 modified@@ -47,6 +47,7 @@ poll: 1 shell: echo "DO_NOT_LOG_ASYNC_TASK_SUCCEEDED" no_log: true + ignore_errors: yes - name: play-level no_log set hosts: testhost
test/integration/targets/no_log/no_log_suboptions_invalid.yml+45 −0 added@@ -0,0 +1,45 @@ +- name: test no log with suboptions + hosts: testhost + gather_facts: no + ignore_errors: yes + + tasks: + - name: Task with suboptions and invalid parameter + module: + secret: SUPREME + invalid: param + subopt_dict: + str_sub_opt1: IDIOM + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: MOCKUP + + subopt_list: + - subopt1: EDUCATED + subopt2: thridstring + - subopt1: FOOTREST + + - name: Task with suboptions as string with invalid parameter + module: + secret: FOOTREST + invalid: param + subopt_dict: str_sub_opt1=CRAFTY + + - name: Task with suboptions with dict instead of list + module: + secret: FELINE + subopt_dict: + str_sub_opt1: CRYSTAL + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: EXPECTANT + subopt_list: + foo: bar + + - name: Task with suboptions with incorrect data type + module: + secret: AGROUND + subopt_dict: 9068.21361 + subopt_list: + - subopt1: GOLIATH + - subopt1: FREEFALL
test/integration/targets/no_log/no_log_suboptions.yml+24 −0 added@@ -0,0 +1,24 @@ +- name: test no log with suboptions + hosts: testhost + gather_facts: no + + tasks: + - name: Task with suboptions + module: + secret: GLAMOROUS + subopt_dict: + str_sub_opt1: AFTERMATH + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: MANPOWER + + subopt_list: + - subopt1: UNTAPPED + subopt2: thridstring + + - subopt1: CONCERNED + + - name: Task with suboptions as string + module: + secret: MARLIN + subopt_dict: str_sub_opt1=FLICK
test/integration/targets/no_log/runme.sh+6 −0 modified@@ -13,3 +13,9 @@ set -eux # no log disabled, should produce 0 censored [ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=yes|grep -c 'output has been hidden')" = "0" ] + +# test no log for sub options +[ "$(ansible-playbook no_log_suboptions.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(MANPOWER|UNTAPPED|CONCERNED|MARLIN|FLICK)')" = "0" ] + +# test invalid data passed to a suboption +[ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(SUPREME|IDIOM|MOCKUP|EDUCATED|FOOTREST|CRAFTY|FELINE|CRYSTAL|EXPECTANT|AGROUND|GOLIATH|FREEFALL)')" = "0" ]
0fd656e9964a[stable-2.7] Properly mask no_log values is sub parameters during failure (#63405)
7 files changed · +151 −1
changelogs/fragments/no-log-sub-options-invalid-parameter.yaml+2 −0 added@@ -0,0 +1,2 @@ +bugfixes: + - '**security issue** - properly hide parameters marked with ``no_log`` in suboptions when invalid parameters are passed to the module (CVE-2019-14858)'
lib/ansible/module_utils/basic.py+28 −1 modified@@ -1665,6 +1665,34 @@ def _handle_no_log_values(self, spec=None, param=None): if no_log_object: self.no_log_values.update(return_values(no_log_object)) + # Get no_log values from suboptions + sub_argument_spec = arg_opts.get('options') + if sub_argument_spec is not None: + wanted_type = arg_opts.get('type') + sub_parameters = param.get(arg_name) + + if sub_parameters is not None: + if wanted_type == 'dict' or (wanted_type == 'list' and arg_opts.get('elements', '') == 'dict'): + # Sub parameters can be a dict or list of dicts. Ensure parameters are always a list. + if not isinstance(sub_parameters, list): + sub_parameters = [sub_parameters] + + for sub_param in sub_parameters: + # Validate dict fields in case they came in as strings + + if isinstance(sub_param, string_types): + sub_param = self._check_type_dict(sub_param) + + try: + if not isinstance(sub_param, Mapping): + raise TypeError("Value '{1}' in the sub parameter field '{0}' must by a {2}, " + "not '{1.__class__.__name__}'".format(arg_name, sub_param, wanted_type)) + except TypeError as te: + self.fail_json(msg="Failure when processing no_log parameters. Module invocation will be hidden. " + "%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'}) + + self._handle_no_log_values(sub_argument_spec, sub_param) + if arg_opts.get('removed_in_version') is not None and arg_name in param: self._deprecations.append({ 'msg': "Param '%s' is deprecated. See the module docs for more information" % arg_name, @@ -2032,7 +2060,6 @@ def _handle_options(self, argument_spec=None, params=None): self._set_fallbacks(spec, param) options_aliases = self._handle_aliases(spec, param) - self._handle_no_log_values(spec, param) options_legal_inputs = list(spec.keys()) + list(options_aliases.keys()) self._check_arguments(self.check_invalid_arguments, spec, param, options_legal_inputs)
test/integration/targets/no_log/library/module.py+45 −0 added@@ -0,0 +1,45 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2019 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec={ + 'state': {}, + 'secret': {'no_log': True}, + 'subopt_dict': { + 'type': 'dict', + 'options': { + 'str_sub_opt1': {'no_log': True}, + 'str_sub_opt2': {}, + 'nested_subopt': { + 'type': 'dict', + 'options': { + 'n_subopt1': {'no_log': True}, + } + } + } + }, + 'subopt_list': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'subopt1': {'no_log': True}, + 'subopt2': {}, + } + } + + } + ) + module.exit_json(msg='done') + + +if __name__ == '__main__': + main()
test/integration/targets/no_log/no_log_local.yml+1 −0 modified@@ -47,6 +47,7 @@ poll: 1 shell: echo "DO_NOT_LOG_ASYNC_TASK_SUCCEEDED" no_log: true + ignore_errors: yes - name: play-level no_log set hosts: testhost
test/integration/targets/no_log/no_log_suboptions_invalid.yml+45 −0 added@@ -0,0 +1,45 @@ +- name: test no log with suboptions + hosts: testhost + gather_facts: no + ignore_errors: yes + + tasks: + - name: Task with suboptions and invalid parameter + module: + secret: SUPREME + invalid: param + subopt_dict: + str_sub_opt1: IDIOM + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: MOCKUP + + subopt_list: + - subopt1: EDUCATED + subopt2: thridstring + - subopt1: FOOTREST + + - name: Task with suboptions as string with invalid parameter + module: + secret: FOOTREST + invalid: param + subopt_dict: str_sub_opt1=CRAFTY + + - name: Task with suboptions with dict instead of list + module: + secret: FELINE + subopt_dict: + str_sub_opt1: CRYSTAL + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: EXPECTANT + subopt_list: + foo: bar + + - name: Task with suboptions with incorrect data type + module: + secret: AGROUND + subopt_dict: 9068.21361 + subopt_list: + - subopt1: GOLIATH + - subopt1: FREEFALL
test/integration/targets/no_log/no_log_suboptions.yml+24 −0 added@@ -0,0 +1,24 @@ +- name: test no log with suboptions + hosts: testhost + gather_facts: no + + tasks: + - name: Task with suboptions + module: + secret: GLAMOROUS + subopt_dict: + str_sub_opt1: AFTERMATH + str_sub_opt2: otherstring + nested_subopt: + n_subopt1: MANPOWER + + subopt_list: + - subopt1: UNTAPPED + subopt2: thridstring + + - subopt1: CONCERNED + + - name: Task with suboptions as string + module: + secret: MARLIN + subopt_dict: str_sub_opt1=FLICK
test/integration/targets/no_log/runme.sh+6 −0 modified@@ -13,3 +13,9 @@ set -eux # no log disabled, should produce 0 censored [ "$(ansible-playbook dynamic.yml -i ../../inventory -vvvvv "$@" -e unsafe_show_logs=yes|grep -c 'output has been hidden')" = "0" ] + +# test no log for sub options +[ "$(ansible-playbook no_log_suboptions.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(MANPOWER|UNTAPPED|CONCERNED|MARLIN|FLICK)')" = "0" ] + +# test invalid data passed to a suboption +[ "$(ansible-playbook no_log_suboptions_invalid.yml -i ../../inventory -vvvvv "$@" | grep -Ec '(SUPREME|IDIOM|MOCKUP|EDUCATED|FOOTREST|CRAFTY|FELINE|CRYSTAL|EXPECTANT|AGROUND|GOLIATH|FREEFALL)')" = "0" ]
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
16- lists.opensuse.org/opensuse-security-announce/2020-04/msg00021.htmlghsavendor-advisoryx_refsource_SUSEWEB
- lists.opensuse.org/opensuse-security-announce/2020-04/msg00026.htmlghsavendor-advisoryx_refsource_SUSEWEB
- access.redhat.com/errata/RHSA-2019:3201ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2019:3202ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2019:3203ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2019:3207ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2020:0756ghsavendor-advisoryx_refsource_REDHATWEB
- github.com/advisories/GHSA-h653-95qw-h2mpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2019-14858ghsaADVISORY
- bugzilla.redhat.com/show_bug.cgighsax_refsource_CONFIRMWEB
- github.com/ansible/ansible/commit/0fd656e9964a91f2e8b1e9bbf78c74661ab9d37bghsaWEB
- github.com/ansible/ansible/commit/3dfb8e81bb5f776a6b00c7a90dd087e85b71f8bbghsaWEB
- github.com/ansible/ansible/commit/87f8d77d70476454f7fe2381bd363a329ce4266cghsaWEB
- github.com/ansible/ansible/commit/f610ed3a4eb87eb557200606279796921fa9b722ghsaWEB
- github.com/ansible/ansible/pull/63405ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/ansible/PYSEC-2019-171.yamlghsaWEB
News mentions
0No linked articles in our index yet.