CVE-2021-20228
Description
A flaw was found in the Ansible Engine 2.9.18, where sensitive info is not masked by default and is not protected by the no_log feature when using the sub-option feature of the basic.py module. This flaw allows an attacker to obtain sensitive information. The highest threat from this vulnerability is to confidentiality.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
In Ansible Engine 2.9.18, the sub-option feature of basic.py module fails to mask sensitive info, allowing information disclosure.
Vulnerability
A flaw exists in Ansible Engine version 2.9.18 where the basic.py module's sub-option feature does not mask sensitive information by default, and the no_log attribute does not properly protect these values. This allows sensitive data to be exposed in logs or output. The affected version is Ansible Engine 2.9.18 [1][2].
Exploitation
An attacker who can observe Ansible task execution logs or output can obtain sensitive information, such as credentials or tokens, that are included as sub-option values. No special privileges are required beyond the ability to view Ansible output [2].
Impact
Successful exploitation leads to unauthorized disclosure of sensitive information, compromising confidentiality. The attacker gains access to data that should have been masked [1][2].
Mitigation
The issue is fixed in Ansible Engine via pull requests #73487 (for version 2.11) and #73493 (for version 2.9), which ensure that sub-option fallback values and defaults are masked with no_log [3][4]. Users should upgrade to patched versions or apply the relevant patches. No known workarounds exist for unpatched versions.
- NVD - CVE-2021-20228
- CVE-2021-20228 - GitHub Advisory Database
- no_log mask suboption fallback values and defaults CVE-2021-20228 by jborean93 · Pull Request #73487 · ansible/ansible
- no_log mask suboption fallback values and defaults CVE-2021-20228 - 2.9 by jborean93 · Pull Request #73493 · ansible/ansible
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ansiblePyPI | >= 2.10.0a1, < 2.10.6rc1 | 2.10.6rc1 |
ansiblePyPI | >= 2.9.0a1, < 2.9.18rc1 | 2.9.18rc1 |
ansiblePyPI | < 2.8.19rc1 | 2.8.19rc1 |
Affected products
74- Ansible/Ansible Enginedescription
- ghsa-coords73 versionspkg:pypi/ansiblepkg:rpm/opensuse/ansible&distro=openSUSE%20Leap%2015.3pkg:rpm/opensuse/ansible&distro=openSUSE%20Leap%2015.4pkg:rpm/opensuse/dracut-saltboot&distro=openSUSE%20Leap%2015.3pkg:rpm/opensuse/dracut-saltboot&distro=openSUSE%20Leap%2015.4pkg:rpm/opensuse/golang-github-QubitProducts-exporter_exporter&distro=openSUSE%20Leap%2015.3pkg:rpm/opensuse/golang-github-QubitProducts-exporter_exporter&distro=openSUSE%20Leap%2015.4pkg:rpm/opensuse/prometheus-blackbox_exporter&distro=openSUSE%20Leap%2015.4pkg:rpm/opensuse/python-hwdata&distro=openSUSE%20Leap%2015.3pkg:rpm/opensuse/python-hwdata&distro=openSUSE%20Leap%2015.4pkg:rpm/opensuse/spacecmd&distro=openSUSE%20Leap%2015.3pkg:rpm/opensuse/spacecmd&distro=openSUSE%20Leap%2015.4pkg:rpm/opensuse/wire&distro=openSUSE%20Leap%2015.4pkg:rpm/suse/ansible&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/ansible&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/ansible&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/ansible&distro=SUSE%20Manager%20Proxy%20Module%204.2pkg:rpm/suse/ansible&distro=SUSE%20Manager%20Proxy%20Module%204.3pkg:rpm/suse/ansible&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/ansible&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/ansible&distro=SUSE%20Package%20Hub%2015%20SP3pkg:rpm/suse/dracut-saltboot&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/dracut-saltboot&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/dracut-saltboot&distro=SUSE%20Manager%20Client%20Tools%20Beta%20for%20SLE%20Micro%205pkg:rpm/suse/golang-github-boynux-squid_exporter&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/golang-github-lusitaniae-apache_exporter&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/golang-github-prometheus-node_exporter&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015-ESPOSpkg:rpm/suse/golang-github-prometheus-node_exporter&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015-LTSSpkg:rpm/suse/golang-github-prometheus-node_exporter&distro=SUSE%20Linux%20Enterprise%20Server%2015-LTSSpkg:rpm/suse/golang-github-prometheus-node_exporter&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2015pkg:rpm/suse/golang-github-prometheus-prometheus&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/golang-github-QubitProducts-exporter_exporter&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/golang-github-QubitProducts-exporter_exporter&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/golang-github-QubitProducts-exporter_exporter&distro=SUSE%20Manager%20Client%20Tools%20Beta%20for%20SLE%20Micro%205pkg:rpm/suse/golang-github-QubitProducts-exporter_exporter&distro=SUSE%20Manager%20Proxy%20Module%204.2pkg:rpm/suse/golang-github-QubitProducts-exporter_exporter&distro=SUSE%20Manager%20Proxy%20Module%204.3pkg:rpm/suse/golang-github-QubitProducts-exporter_exporter&distro=SUSE%20Manager%20Server%20Module%204.2pkg:rpm/suse/golang-github-QubitProducts-exporter_exporter&distro=SUSE%20Manager%20Server%20Module%204.3pkg:rpm/suse/grafana&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/mgr-daemon&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/mgr-push&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/mgr-virtualization&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/prometheus-blackbox_exporter&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/prometheus-blackbox_exporter&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/prometheus-blackbox_exporter&distro=SUSE%20Manager%20Client%20Tools%20Beta%20for%20SLE%20Micro%205pkg:rpm/suse/prometheus-blackbox_exporter&distro=SUSE%20Manager%20Proxy%20Module%204.2pkg:rpm/suse/prometheus-blackbox_exporter&distro=SUSE%20Manager%20Proxy%20Module%204.3pkg:rpm/suse/prometheus-postgres_exporter&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/python-hwdata&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/python-hwdata&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/python-hwdata&distro=SUSE%20Manager%20Proxy%20Module%204.1pkg:rpm/suse/python-hwdata&distro=SUSE%20Manager%20Proxy%20Module%204.2pkg:rpm/suse/python-hwdata&distro=SUSE%20Manager%20Proxy%20Module%204.3pkg:rpm/suse/python-hwdata&distro=SUSE%20Manager%20Server%20Module%204.1pkg:rpm/suse/python-hwdata&distro=SUSE%20Manager%20Server%20Module%204.2pkg:rpm/suse/python-hwdata&distro=SUSE%20Manager%20Server%20Module%204.3pkg:rpm/suse/python-pyvmomi&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/rhnlib&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/spacecmd&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/spacecmd&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/spacewalk-client-tools&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/spacewalk-client-tools&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/supportutils-plugin-salt&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/supportutils-plugin-susemanager-client&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/uyuni-common-libs&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/uyuni-common-libs&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/uyuni-proxy-systemd-services&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/uyuni-proxy-systemd-services&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/uyuni-proxy-systemd-services&distro=SUSE%20Manager%20Client%20Tools%20Beta%20for%20SLE%20Micro%205pkg:rpm/suse/zypp-plugin-spacewalk&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/zypp-plugin-spacewalk&distro=SUSE%20Manager%20Proxy%20Module%204.1pkg:rpm/suse/zypp-plugin-spacewalk&distro=SUSE%20Manager%20Proxy%20Module%204.2pkg:rpm/suse/zypp-plugin-spacewalk&distro=SUSE%20Manager%20Proxy%20Module%204.3
>= 2.10.0a1, < 2.10.6rc1+ 72 more
- (no CPE)range: >= 2.10.0a1, < 2.10.6rc1
- (no CPE)range: < 2.9.27-150000.1.14.1
- (no CPE)range: < 2.9.27-150000.1.14.1
- (no CPE)range: < 0.1.1657643023.0d694ce-150000.1.35.1
- (no CPE)range: < 0.1.1657643023.0d694ce-150000.1.35.1
- (no CPE)range: < 0.4.0-150000.1.15.1
- (no CPE)range: < 0.4.0-150000.1.15.1
- (no CPE)range: < 0.19.0-150000.1.11.1
- (no CPE)range: < 2.3.5-150000.3.9.1
- (no CPE)range: < 2.3.5-150000.3.9.1
- (no CPE)range: < 4.3.14-150000.3.83.1
- (no CPE)range: < 4.3.14-150000.3.83.1
- (no CPE)range: < 0.5.0-150000.1.6.1
- (no CPE)range: < 2.9.22-3.18.1
- (no CPE)range: < 2.9.27-150000.1.14.1
- (no CPE)range: < 2.9.27-159000.3.9.1
- (no CPE)range: < 2.9.27-150000.1.14.1
- (no CPE)range: < 2.9.27-150000.1.14.1
- (no CPE)range: < 2.9.22-3.18.1
- (no CPE)range: < 2.9.22-3.18.1
- (no CPE)range: < 2.9.21-bp153.2.3.1
- (no CPE)range: < 0.1.1657643023.0d694ce-150000.1.35.1
- (no CPE)range: < 0.1.1681904360.84ef141-159000.3.30.1
- (no CPE)range: < 0.1.1681904360.84ef141-159000.3.30.1
- (no CPE)range: < 1.6-159000.4.9.1
- (no CPE)range: < 1.0.0-159000.4.12.1
- (no CPE)range: < 1.3.0-150000.3.15.1
- (no CPE)range: < 1.3.0-150000.3.15.1
- (no CPE)range: < 1.3.0-150000.3.15.1
- (no CPE)range: < 1.3.0-150000.3.15.1
- (no CPE)range: < 2.45.0-159000.6.33.1
- (no CPE)range: < 0.4.0-150000.1.15.1
- (no CPE)range: < 0.4.0-159000.4.6.1
- (no CPE)range: < 0.4.0-159000.4.6.1
- (no CPE)range: < 0.4.0-150000.1.15.1
- (no CPE)range: < 0.4.0-150000.1.15.1
- (no CPE)range: < 0.4.0-150000.1.15.1
- (no CPE)range: < 0.4.0-150000.1.15.1
- (no CPE)range: < 9.5.8-159000.4.24.1
- (no CPE)range: < 4.3.5-150000.1.35.1
- (no CPE)range: < 5.0.1-159000.4.21.1
- (no CPE)range: < 4.3.6-150000.1.32.1
- (no CPE)range: < 0.19.0-150000.1.11.1
- (no CPE)range: < 0.24.0-159000.3.6.1
- (no CPE)range: < 0.24.0-159000.3.6.1
- (no CPE)range: < 0.19.0-150000.1.11.1
- (no CPE)range: < 0.19.0-150000.1.11.1
- (no CPE)range: < 0.10.1-159000.3.6.1
- (no CPE)range: < 2.3.5-150000.3.9.1
- (no CPE)range: < 2.3.5-159000.5.13.1
- (no CPE)range: < 2.3.5-150000.3.9.1
- (no CPE)range: < 2.3.5-150000.3.9.1
- (no CPE)range: < 2.3.5-150000.3.9.1
- (no CPE)range: < 2.3.5-150000.3.9.1
- (no CPE)range: < 2.3.5-150000.3.9.1
- (no CPE)range: < 2.3.5-150000.3.9.1
- (no CPE)range: < 6.7.3-159000.3.6.1
- (no CPE)range: < 5.0.1-159000.6.30.1
- (no CPE)range: < 4.3.14-150000.3.83.1
- (no CPE)range: < 5.0.1-159000.6.42.1
- (no CPE)range: < 4.3.11-150000.3.65.1
- (no CPE)range: < 5.0.1-159000.6.48.1
- (no CPE)range: < 1.2.2-159000.5.9.1
- (no CPE)range: < 5.0.1-159000.6.15.1
- (no CPE)range: < 4.3.5-150000.1.24.1
- (no CPE)range: < 5.0.1-159000.3.33.1
- (no CPE)range: < 4.3.6-150000.1.6.1
- (no CPE)range: < 5.0.1-159000.3.9.1
- (no CPE)range: < 5.0.1-159000.3.9.1
- (no CPE)range: < 1.0.13-150000.3.32.1
- (no CPE)range: < 1.0.13-150000.3.32.1
- (no CPE)range: < 1.0.13-150000.3.32.1
- (no CPE)range: < 1.0.13-150000.3.32.1
Patches
3e41d1f0a3fd6no_log mask suboption fallback values and defaults CVE-2021-20228 (#73487) (#73494)
7 files changed · +125 −11
changelogs/fragments/no_log-fallback.yml+2 −0 added@@ -0,0 +1,2 @@ +security_fixes: + - '**security issue** - Mask default and fallback values for ``no_log`` module options (CVE-2021-20228)'
lib/ansible/module_utils/basic.py+17 −11 modified@@ -712,6 +712,9 @@ def __init__(self, argument_spec, bypass_checks=False, no_log=False, if k not in self.argument_spec: self.argument_spec[k] = v + # Save parameter values that should never be logged + self.no_log_values = set() + self._load_params() self._set_fallbacks() @@ -723,8 +726,6 @@ def __init__(self, argument_spec, bypass_checks=False, no_log=False, print('\n{"failed": true, "msg": "Module alias error: %s"}' % to_native(e)) sys.exit(1) - # Save parameter values that should never be logged - self.no_log_values = set() self._handle_no_log_values() # check the locale as set by the current environment, and reset to @@ -1944,14 +1945,15 @@ def _set_defaults(self, pre=True, spec=None, param=None): param = self.params for (k, v) in spec.items(): default = v.get('default', None) - if pre is True: - # this prevents setting defaults on required items - if default is not None and k not in param: - param[k] = default - else: - # make sure things without a default still get set None - if k not in param: - param[k] = default + + # This prevents setting defaults on required items on the 1st run, + # otherwise will set things without a default to None on the 2nd. + if k not in param and (default is not None or not pre): + # Make sure any default value for no_log fields are masked. + if v.get('no_log', False) and default: + self.no_log_values.add(default) + + param[k] = default def _set_fallbacks(self, spec=None, param=None): if spec is None: @@ -1971,9 +1973,13 @@ def _set_fallbacks(self, spec=None, param=None): else: fallback_args = item try: - param[k] = fallback_strategy(*fallback_args, **fallback_kwargs) + fallback_value = fallback_strategy(*fallback_args, **fallback_kwargs) except AnsibleFallbackNotFound: continue + else: + if v.get('no_log', False) and fallback_value: + self.no_log_values.add(fallback_value) + param[k] = fallback_value def _load_params(self): ''' read the input and set the params attribute.
test/integration/targets/module_utils/callback/pure_json.py+31 −0 added@@ -0,0 +1,31 @@ +# (c) 2021 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 + +DOCUMENTATION = ''' + name: pure_json + type: stdout + short_description: only outputs the module results as json +''' + +import json + +from ansible.plugins.callback import CallbackBase + + +class CallbackModule(CallbackBase): + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'stdout' + CALLBACK_NAME = 'pure_json' + + def v2_runner_on_failed(self, result, ignore_errors=False): + self._display.display(json.dumps(result._result)) + + def v2_runner_on_ok(self, result): + self._display.display(json.dumps(result._result)) + + def v2_runner_on_skipped(self, result): + self._display.display(json.dumps(result._result))
test/integration/targets/module_utils/library/test_no_log.py+35 −0 added@@ -0,0 +1,35 @@ +#!/usr/bin/python +# (c) 2021 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, env_fallback + + +def main(): + module = AnsibleModule( + argument_spec=dict( + explicit_pass=dict(type='str', no_log=True), + fallback_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_ENV'])), + default_pass=dict(type='str', no_log=True, default='zyx'), + normal=dict(type='str', default='plaintext'), + suboption=dict( + type='dict', + options=dict( + explicit_sub_pass=dict(type='str', no_log=True), + fallback_sub_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_SUB_ENV'])), + default_sub_pass=dict(type='str', no_log=True, default='xvu'), + normal=dict(type='str', default='plaintext'), + ), + ), + ), + ) + + module.exit_json(changed=False) + + +if __name__ == '__main__': + main()
test/integration/targets/module_utils/module_utils_test_no_log.yml+9 −0 added@@ -0,0 +1,9 @@ +# This is called by module_utils_vvvvv.yml with a custom callback +- hosts: testhost + gather_facts: no + tasks: + - name: Check no_log invocation results + test_no_log: + explicit_pass: abc + suboption: + explicit_sub_pass: def
test/integration/targets/module_utils/module_utils_vvvvv.yml+27 −0 added@@ -0,0 +1,27 @@ +- hosts: testhost + gather_facts: no + tasks: + # Invocation usually is output with 3vs or more, our callback plugin displays it anyway + - name: Check no_log invocation results + command: ansible-playbook -i {{ inventory_file }} module_utils_test_no_log.yml + environment: + ANSIBLE_CALLBACK_PLUGINS: callback + ANSIBLE_STDOUT_CALLBACK: pure_json + SECRET_ENV: ghi + SECRET_SUB_ENV: jkl + register: no_log_invocation + + - set_fact: + no_log_invocation: '{{ no_log_invocation.stdout | trim | from_json }}' + + - name: check no log values from fallback or default are masked + assert: + that: + - no_log_invocation.invocation.module_args.default_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.explicit_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.fallback_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.normal == 'plaintext' + - no_log_invocation.invocation.module_args.suboption.default_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.suboption.explicit_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.suboption.fallback_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.suboption.normal == 'plaintext'
test/integration/targets/module_utils/runme.sh+4 −0 modified@@ -4,6 +4,10 @@ set -eux ANSIBLE_ROLES_PATH=../ ansible-playbook module_utils_basic_setcwd.yml -i ../../inventory "$@" +# Keep the -vvvvv here. This acts as a test for testing that higher verbosity +# doesn't traceback with unicode in the custom module_utils directory path. +ansible-playbook module_utils_vvvvv.yml -i ../../inventory -vvvvv "$@" + ansible-playbook module_utils_test.yml -i ../../inventory -v "$@" ANSIBLE_MODULE_UTILS=other_mu_dir ansible-playbook module_utils_envvar.yml -i ../../inventory -v "$@"
49ebd509df9dno_log mask suboption fallback values and defaults CVE-2021-20228 (#73487) (#73493)
7 files changed · +125 −11
changelogs/fragments/no_log-fallback.yml+2 −0 added@@ -0,0 +1,2 @@ +security_fixes: + - '**security issue** - Mask default and fallback values for ``no_log`` module options (CVE-2021-20228)'
lib/ansible/module_utils/basic.py+17 −11 modified@@ -725,6 +725,9 @@ def __init__(self, argument_spec, bypass_checks=False, no_log=False, if k not in self.argument_spec: self.argument_spec[k] = v + # Save parameter values that should never be logged + self.no_log_values = set() + self._load_params() self._set_fallbacks() @@ -736,8 +739,6 @@ def __init__(self, argument_spec, bypass_checks=False, no_log=False, print('\n{"failed": true, "msg": "Module alias error: %s"}' % to_native(e)) sys.exit(1) - # Save parameter values that should never be logged - self.no_log_values = set() self._handle_no_log_values() # check the locale as set by the current environment, and reset to @@ -1926,14 +1927,15 @@ def _set_defaults(self, pre=True, spec=None, param=None): param = self.params for (k, v) in spec.items(): default = v.get('default', None) - if pre is True: - # this prevents setting defaults on required items - if default is not None and k not in param: - param[k] = default - else: - # make sure things without a default still get set None - if k not in param: - param[k] = default + + # This prevents setting defaults on required items on the 1st run, + # otherwise will set things without a default to None on the 2nd. + if k not in param and (default is not None or not pre): + # Make sure any default value for no_log fields are masked. + if v.get('no_log', False) and default: + self.no_log_values.add(default) + + param[k] = default def _set_fallbacks(self, spec=None, param=None): if spec is None: @@ -1953,9 +1955,13 @@ def _set_fallbacks(self, spec=None, param=None): else: fallback_args = item try: - param[k] = fallback_strategy(*fallback_args, **fallback_kwargs) + fallback_value = fallback_strategy(*fallback_args, **fallback_kwargs) except AnsibleFallbackNotFound: continue + else: + if v.get('no_log', False) and fallback_value: + self.no_log_values.add(fallback_value) + param[k] = fallback_value def _load_params(self): ''' read the input and set the params attribute.
test/integration/targets/module_utils/callback/pure_json.py+31 −0 added@@ -0,0 +1,31 @@ +# (c) 2021 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 + +DOCUMENTATION = ''' + name: pure_json + type: stdout + short_description: only outputs the module results as json +''' + +import json + +from ansible.plugins.callback import CallbackBase + + +class CallbackModule(CallbackBase): + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'stdout' + CALLBACK_NAME = 'pure_json' + + def v2_runner_on_failed(self, result, ignore_errors=False): + self._display.display(json.dumps(result._result)) + + def v2_runner_on_ok(self, result): + self._display.display(json.dumps(result._result)) + + def v2_runner_on_skipped(self, result): + self._display.display(json.dumps(result._result))
test/integration/targets/module_utils/library/test_no_log.py+35 −0 added@@ -0,0 +1,35 @@ +#!/usr/bin/python +# (c) 2021 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, env_fallback + + +def main(): + module = AnsibleModule( + argument_spec=dict( + explicit_pass=dict(type='str', no_log=True), + fallback_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_ENV'])), + default_pass=dict(type='str', no_log=True, default='zyx'), + normal=dict(type='str', default='plaintext'), + suboption=dict( + type='dict', + options=dict( + explicit_sub_pass=dict(type='str', no_log=True), + fallback_sub_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_SUB_ENV'])), + default_sub_pass=dict(type='str', no_log=True, default='xvu'), + normal=dict(type='str', default='plaintext'), + ), + ), + ), + ) + + module.exit_json(changed=False) + + +if __name__ == '__main__': + main()
test/integration/targets/module_utils/module_utils_test_no_log.yml+9 −0 added@@ -0,0 +1,9 @@ +# This is called by module_utils_vvvvv.yml with a custom callback +- hosts: testhost + gather_facts: no + tasks: + - name: Check no_log invocation results + test_no_log: + explicit_pass: abc + suboption: + explicit_sub_pass: def
test/integration/targets/module_utils/module_utils_vvvvv.yml+27 −0 added@@ -0,0 +1,27 @@ +- hosts: testhost + gather_facts: no + tasks: + # Invocation usually is output with 3vs or more, our callback plugin displays it anyway + - name: Check no_log invocation results + command: ansible-playbook -i {{ inventory_file }} module_utils_test_no_log.yml + environment: + ANSIBLE_CALLBACK_PLUGINS: callback + ANSIBLE_STDOUT_CALLBACK: pure_json + SECRET_ENV: ghi + SECRET_SUB_ENV: jkl + register: no_log_invocation + + - set_fact: + no_log_invocation: '{{ no_log_invocation.stdout | trim | from_json }}' + + - name: check no log values from fallback or default are masked + assert: + that: + - no_log_invocation.invocation.module_args.default_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.explicit_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.fallback_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.normal == 'plaintext' + - no_log_invocation.invocation.module_args.suboption.default_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.suboption.explicit_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.suboption.fallback_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.suboption.normal == 'plaintext'
test/integration/targets/module_utils/runme.sh+4 −0 modified@@ -2,5 +2,9 @@ set -eux +# Keep the -vvvvv here. This acts as a test for testing that higher verbosity +# doesn't traceback with unicode in the custom module_utils directory path. +ansible-playbook module_utils_vvvvv.yml -i ../../inventory -vvvvv "$@" + ansible-playbook module_utils_test.yml -i ../../inventory -v "$@" ANSIBLE_MODULE_UTILS=other_mu_dir ansible-playbook module_utils_envvar.yml -i ../../inventory -v "$@"
f8ff395d817cno_log mask suboption fallback values and defaults CVE-2021-20228 (#73487) (#73492)
7 files changed · +125 −11
changelogs/fragments/no_log-fallback.yml+2 −0 added@@ -0,0 +1,2 @@ +security_fixes: + - '**security issue** - Mask default and fallback values for ``no_log`` module options (CVE-2021-20228)'
lib/ansible/module_utils/basic.py+17 −11 modified@@ -720,6 +720,9 @@ def __init__(self, argument_spec, bypass_checks=False, no_log=False, if k not in self.argument_spec: self.argument_spec[k] = v + # Save parameter values that should never be logged + self.no_log_values = set() + self._load_params() self._set_fallbacks() @@ -731,8 +734,6 @@ def __init__(self, argument_spec, bypass_checks=False, no_log=False, print('\n{"failed": true, "msg": "Module alias error: %s"}' % to_native(e)) sys.exit(1) - # Save parameter values that should never be logged - self.no_log_values = set() self._handle_no_log_values() # check the locale as set by the current environment, and reset to @@ -1893,14 +1894,15 @@ def _set_defaults(self, pre=True, spec=None, param=None): param = self.params for (k, v) in spec.items(): default = v.get('default', None) - if pre is True: - # this prevents setting defaults on required items - if default is not None and k not in param: - param[k] = default - else: - # make sure things without a default still get set None - if k not in param: - param[k] = default + + # This prevents setting defaults on required items on the 1st run, + # otherwise will set things without a default to None on the 2nd. + if k not in param and (default is not None or not pre): + # Make sure any default value for no_log fields are masked. + if v.get('no_log', False) and default: + self.no_log_values.add(default) + + param[k] = default def _set_fallbacks(self, spec=None, param=None): if spec is None: @@ -1920,9 +1922,13 @@ def _set_fallbacks(self, spec=None, param=None): else: fallback_args = item try: - param[k] = fallback_strategy(*fallback_args, **fallback_kwargs) + fallback_value = fallback_strategy(*fallback_args, **fallback_kwargs) except AnsibleFallbackNotFound: continue + else: + if v.get('no_log', False) and fallback_value: + self.no_log_values.add(fallback_value) + param[k] = fallback_value def _load_params(self): ''' read the input and set the params attribute.
test/integration/targets/module_utils/callback/pure_json.py+31 −0 added@@ -0,0 +1,31 @@ +# (c) 2021 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 + +DOCUMENTATION = ''' + name: pure_json + type: stdout + short_description: only outputs the module results as json +''' + +import json + +from ansible.plugins.callback import CallbackBase + + +class CallbackModule(CallbackBase): + + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'stdout' + CALLBACK_NAME = 'pure_json' + + def v2_runner_on_failed(self, result, ignore_errors=False): + self._display.display(json.dumps(result._result)) + + def v2_runner_on_ok(self, result): + self._display.display(json.dumps(result._result)) + + def v2_runner_on_skipped(self, result): + self._display.display(json.dumps(result._result))
test/integration/targets/module_utils/library/test_no_log.py+35 −0 added@@ -0,0 +1,35 @@ +#!/usr/bin/python +# (c) 2021 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, env_fallback + + +def main(): + module = AnsibleModule( + argument_spec=dict( + explicit_pass=dict(type='str', no_log=True), + fallback_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_ENV'])), + default_pass=dict(type='str', no_log=True, default='zyx'), + normal=dict(type='str', default='plaintext'), + suboption=dict( + type='dict', + options=dict( + explicit_sub_pass=dict(type='str', no_log=True), + fallback_sub_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_SUB_ENV'])), + default_sub_pass=dict(type='str', no_log=True, default='xvu'), + normal=dict(type='str', default='plaintext'), + ), + ), + ), + ) + + module.exit_json(changed=False) + + +if __name__ == '__main__': + main()
test/integration/targets/module_utils/module_utils_test_no_log.yml+9 −0 added@@ -0,0 +1,9 @@ +# This is called by module_utils_vvvvv.yml with a custom callback +- hosts: testhost + gather_facts: no + tasks: + - name: Check no_log invocation results + test_no_log: + explicit_pass: abc + suboption: + explicit_sub_pass: def
test/integration/targets/module_utils/module_utils_vvvvv.yml+27 −0 added@@ -0,0 +1,27 @@ +- hosts: testhost + gather_facts: no + tasks: + # Invocation usually is output with 3vs or more, our callback plugin displays it anyway + - name: Check no_log invocation results + command: ansible-playbook -i {{ inventory_file }} module_utils_test_no_log.yml + environment: + ANSIBLE_CALLBACK_PLUGINS: callback + ANSIBLE_STDOUT_CALLBACK: pure_json + SECRET_ENV: ghi + SECRET_SUB_ENV: jkl + register: no_log_invocation + + - set_fact: + no_log_invocation: '{{ no_log_invocation.stdout | trim | from_json }}' + + - name: check no log values from fallback or default are masked + assert: + that: + - no_log_invocation.invocation.module_args.default_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.explicit_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.fallback_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.normal == 'plaintext' + - no_log_invocation.invocation.module_args.suboption.default_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.suboption.explicit_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.suboption.fallback_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + - no_log_invocation.invocation.module_args.suboption.normal == 'plaintext'
test/integration/targets/module_utils/runme.sh+4 −0 modified@@ -2,5 +2,9 @@ set -eux +# Keep the -vvvvv here. This acts as a test for testing that higher verbosity +# doesn't traceback with unicode in the custom module_utils directory path. +ansible-playbook module_utils_vvvvv.yml -i ../../inventory -vvvvv "$@" + ansible-playbook module_utils_test.yml -i ../../inventory -v "$@" ANSIBLE_MODULE_UTILS=other_mu_dir ansible-playbook module_utils_envvar.yml -i ../../inventory -v "$@"
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
12- github.com/advisories/GHSA-5rrg-rr89-x9mvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-20228ghsaADVISORY
- www.debian.org/security/2021/dsa-4950ghsavendor-advisoryx_refsource_DEBIANWEB
- bugzilla.redhat.com/show_bug.cgighsax_refsource_MISCWEB
- github.com/ansible/ansible/commit/49ebd509df9de1c1fc1bcee00e79a835dd00662cghsaWEB
- github.com/ansible/ansible/commit/e41d1f0a3fd6c466192e7e24accd3d1c6501111bghsaWEB
- github.com/ansible/ansible/commit/f8ff395d817c3eddc050f809919c15dfb5796120ghsaWEB
- github.com/ansible/ansible/pull/73487ghsax_refsource_MISCWEB
- github.com/ansible/ansible/pull/73492ghsaWEB
- github.com/ansible/ansible/pull/73493ghsaWEB
- github.com/ansible/ansible/pull/73494ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/ansible/PYSEC-2021-1.yamlghsaWEB
News mentions
0No linked articles in our index yet.