Browse code

no_log mask suboption fallback values and defaults CVE-2021-20228 (#73487) (#73494)

(cherry picked from commit 0cdc410dce6658e93c06fa27e0100ddbb11e7015)

Jordan Borean authored on 2021/02/07 16:11:27
Showing 7 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+security_fixes:
1
+  - '**security issue** - Mask default and fallback values for ``no_log`` module options (CVE-2021-20228)'
... ...
@@ -712,6 +712,9 @@ class AnsibleModule(object):
712 712
                 if k not in self.argument_spec:
713 713
                     self.argument_spec[k] = v
714 714
 
715
+        # Save parameter values that should never be logged
716
+        self.no_log_values = set()
717
+
715 718
         self._load_params()
716 719
         self._set_fallbacks()
717 720
 
... ...
@@ -723,8 +726,6 @@ class AnsibleModule(object):
723 723
             print('\n{"failed": true, "msg": "Module alias error: %s"}' % to_native(e))
724 724
             sys.exit(1)
725 725
 
726
-        # Save parameter values that should never be logged
727
-        self.no_log_values = set()
728 726
         self._handle_no_log_values()
729 727
 
730 728
         # check the locale as set by the current environment, and reset to
... ...
@@ -1944,14 +1945,15 @@ class AnsibleModule(object):
1944 1944
             param = self.params
1945 1945
         for (k, v) in spec.items():
1946 1946
             default = v.get('default', None)
1947
-            if pre is True:
1948
-                # this prevents setting defaults on required items
1949
-                if default is not None and k not in param:
1950
-                    param[k] = default
1951
-            else:
1952
-                # make sure things without a default still get set None
1953
-                if k not in param:
1954
-                    param[k] = default
1947
+
1948
+            # This prevents setting defaults on required items on the 1st run,
1949
+            # otherwise will set things without a default to None on the 2nd.
1950
+            if k not in param and (default is not None or not pre):
1951
+                # Make sure any default value for no_log fields are masked.
1952
+                if v.get('no_log', False) and default:
1953
+                    self.no_log_values.add(default)
1954
+
1955
+                param[k] = default
1955 1956
 
1956 1957
     def _set_fallbacks(self, spec=None, param=None):
1957 1958
         if spec is None:
... ...
@@ -1971,9 +1973,13 @@ class AnsibleModule(object):
1971 1971
                     else:
1972 1972
                         fallback_args = item
1973 1973
                 try:
1974
-                    param[k] = fallback_strategy(*fallback_args, **fallback_kwargs)
1974
+                    fallback_value = fallback_strategy(*fallback_args, **fallback_kwargs)
1975 1975
                 except AnsibleFallbackNotFound:
1976 1976
                     continue
1977
+                else:
1978
+                    if v.get('no_log', False) and fallback_value:
1979
+                        self.no_log_values.add(fallback_value)
1980
+                    param[k] = fallback_value
1977 1981
 
1978 1982
     def _load_params(self):
1979 1983
         ''' read the input and set the params attribute.
1980 1984
new file mode 100644
... ...
@@ -0,0 +1,31 @@
0
+# (c) 2021 Ansible Project
1
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
2
+
3
+from __future__ import (absolute_import, division, print_function)
4
+__metaclass__ = type
5
+
6
+DOCUMENTATION = '''
7
+    name: pure_json
8
+    type: stdout
9
+    short_description: only outputs the module results as json
10
+'''
11
+
12
+import json
13
+
14
+from ansible.plugins.callback import CallbackBase
15
+
16
+
17
+class CallbackModule(CallbackBase):
18
+
19
+    CALLBACK_VERSION = 2.0
20
+    CALLBACK_TYPE = 'stdout'
21
+    CALLBACK_NAME = 'pure_json'
22
+
23
+    def v2_runner_on_failed(self, result, ignore_errors=False):
24
+        self._display.display(json.dumps(result._result))
25
+
26
+    def v2_runner_on_ok(self, result):
27
+        self._display.display(json.dumps(result._result))
28
+
29
+    def v2_runner_on_skipped(self, result):
30
+        self._display.display(json.dumps(result._result))
0 31
new file mode 100644
... ...
@@ -0,0 +1,35 @@
0
+#!/usr/bin/python
1
+# (c) 2021 Ansible Project
2
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
+
4
+from __future__ import absolute_import, division, print_function
5
+__metaclass__ = type
6
+
7
+
8
+from ansible.module_utils.basic import AnsibleModule, env_fallback
9
+
10
+
11
+def main():
12
+    module = AnsibleModule(
13
+        argument_spec=dict(
14
+            explicit_pass=dict(type='str', no_log=True),
15
+            fallback_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_ENV'])),
16
+            default_pass=dict(type='str', no_log=True, default='zyx'),
17
+            normal=dict(type='str', default='plaintext'),
18
+            suboption=dict(
19
+                type='dict',
20
+                options=dict(
21
+                    explicit_sub_pass=dict(type='str', no_log=True),
22
+                    fallback_sub_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['SECRET_SUB_ENV'])),
23
+                    default_sub_pass=dict(type='str', no_log=True, default='xvu'),
24
+                    normal=dict(type='str', default='plaintext'),
25
+                ),
26
+            ),
27
+        ),
28
+    )
29
+
30
+    module.exit_json(changed=False)
31
+
32
+
33
+if __name__ == '__main__':
34
+    main()
0 35
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+# This is called by module_utils_vvvvv.yml with a custom callback
1
+- hosts: testhost
2
+  gather_facts: no
3
+  tasks:
4
+    - name: Check no_log invocation results
5
+      test_no_log:
6
+        explicit_pass: abc
7
+        suboption:
8
+          explicit_sub_pass: def
0 9
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+- hosts: testhost
1
+  gather_facts: no
2
+  tasks:
3
+  # Invocation usually is output with 3vs or more, our callback plugin displays it anyway
4
+  - name: Check no_log invocation results
5
+    command: ansible-playbook -i {{ inventory_file }} module_utils_test_no_log.yml
6
+    environment:
7
+      ANSIBLE_CALLBACK_PLUGINS: callback
8
+      ANSIBLE_STDOUT_CALLBACK: pure_json
9
+      SECRET_ENV: ghi
10
+      SECRET_SUB_ENV: jkl
11
+    register: no_log_invocation
12
+
13
+  - set_fact:
14
+      no_log_invocation: '{{ no_log_invocation.stdout | trim | from_json }}'
15
+
16
+  - name: check no log values from fallback or default are masked
17
+    assert:
18
+      that:
19
+      - no_log_invocation.invocation.module_args.default_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
20
+      - no_log_invocation.invocation.module_args.explicit_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
21
+      - no_log_invocation.invocation.module_args.fallback_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
22
+      - no_log_invocation.invocation.module_args.normal == 'plaintext'
23
+      - no_log_invocation.invocation.module_args.suboption.default_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
24
+      - no_log_invocation.invocation.module_args.suboption.explicit_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
25
+      - no_log_invocation.invocation.module_args.suboption.fallback_sub_pass == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
26
+      - no_log_invocation.invocation.module_args.suboption.normal == 'plaintext'
... ...
@@ -4,6 +4,10 @@ set -eux
4 4
 
5 5
 ANSIBLE_ROLES_PATH=../ ansible-playbook module_utils_basic_setcwd.yml -i ../../inventory "$@"
6 6
 
7
+# Keep the -vvvvv here. This acts as a test for testing that higher verbosity
8
+# doesn't traceback with unicode in the custom module_utils directory path.
9
+ansible-playbook module_utils_vvvvv.yml -i ../../inventory -vvvvv "$@"
10
+
7 11
 ansible-playbook module_utils_test.yml -i ../../inventory -v "$@"
8 12
 ANSIBLE_MODULE_UTILS=other_mu_dir ansible-playbook module_utils_envvar.yml -i ../../inventory -v "$@"
9 13