diff --git a/changelogs/fragments/avoid-set_options-leak.yaml b/changelogs/fragments/avoid-set_options-leak.yaml new file mode 100644 index 00000000000..9680f444e6c --- /dev/null +++ b/changelogs/fragments/avoid-set_options-leak.yaml @@ -0,0 +1,5 @@ +--- +security_fixes: + - Do not include params in exception when a call to ``set_options`` fails. + Additionally, block the exception that is returned from being displayed to stdout. + (CVE-2021-3620) diff --git a/lib/ansible/cli/scripts/ansible_connection_cli_stub.py b/lib/ansible/cli/scripts/ansible_connection_cli_stub.py index 1b85c1c34a6..4cb09d57c7e 100755 --- a/lib/ansible/cli/scripts/ansible_connection_cli_stub.py +++ b/lib/ansible/cli/scripts/ansible_connection_cli_stub.py @@ -100,7 +100,11 @@ class ConnectionProcess(object): self.play_context.private_key_file = os.path.join(self.original_path, self.play_context.private_key_file) self.connection = connection_loader.get(self.play_context.connection, self.play_context, '/dev/null', task_uuid=self._task_uuid, ansible_playbook_pid=self._ansible_playbook_pid) - self.connection.set_options(var_options=variables) + try: + self.connection.set_options(var_options=variables) + except ConnectionError as exc: + messages.append(('debug', to_text(exc))) + raise ConnectionError('Unable to decode JSON from response set_options. See the debug log for more information.') self.connection._socket_path = self.socket_path self.srv.register(self.connection) @@ -302,7 +306,11 @@ def main(): else: messages.append(('vvvv', 'found existing local domain socket, using it!')) conn = Connection(socket_path) - conn.set_options(var_options=variables) + try: + conn.set_options(var_options=variables) + except ConnectionError as exc: + messages.append(('debug', to_text(exc))) + raise ConnectionError('Unable to decode JSON from response set_options. See the debug log for more information.') pc_data = to_text(init_data) try: conn.update_play_context(pc_data) diff --git a/lib/ansible/module_utils/connection.py b/lib/ansible/module_utils/connection.py index a76fdb6bcc3..fd0b134087c 100644 --- a/lib/ansible/module_utils/connection.py +++ b/lib/ansible/module_utils/connection.py @@ -163,6 +163,11 @@ class Connection(object): try: response = json.loads(out) except ValueError: + # set_option(s) has sensitive info, and the details are unlikely to matter anyway + if name.startswith("set_option"): + raise ConnectionError( + "Unable to decode JSON from response to {0}. Received '{1}'.".format(name, out) + ) params = [repr(arg) for arg in args] + ['{0}={1!r}'.format(k, v) for k, v in iteritems(kwargs)] params = ', '.join(params) raise ConnectionError( diff --git a/test/units/module_utils/test_connection.py b/test/units/module_utils/test_connection.py new file mode 100644 index 00000000000..bd0285b3a72 --- /dev/null +++ b/test/units/module_utils/test_connection.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, Matt Martz +# 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 import connection + +import pytest + + +def test_set_options_credential_exposure(): + def send(data): + return '{' + + c = connection.Connection(connection.__file__) + c.send = send + with pytest.raises(connection.ConnectionError) as excinfo: + c._exec_jsonrpc('set_options', become_pass='password') + + assert 'password' not in str(excinfo.value)