From c52b423a7290911fa4835c69fa45d58fea4dd08d Mon Sep 17 00:00:00 2001 From: starlet-dx <15929766099@163.com> Date: Fri, 17 Sep 2021 18:09:57 +0800 Subject: [PATCH] fix CVE-2019-14904 CVE-2020-10684 CVE-2020-10729 CVE-2020-1735-to-CVE-2020-1740 CVE-2020-1753 CVE-2021-20191 (cherry picked from commit ebf023f03ad09762c8147ad8c963a51b60de62ff) --- CVE-2019-14904.patch | 68 ++++++++ CVE-2020-10684.patch | 111 +++++++++++++ CVE-2020-10729.patch | 22 +++ CVE-2020-1735.patch | 215 ++++++++++++++++++++++++ CVE-2020-1736.patch | 74 +++++++++ CVE-2020-1737.patch | 28 ++++ CVE-2020-1738.patch | 69 ++++++++ CVE-2020-1739.patch | 384 +++++++++++++++++++++++++++++++++++++++++++ CVE-2020-1740.patch | 229 ++++++++++++++++++++++++++ CVE-2020-1753.patch | 39 +++++ CVE-2021-20191.patch | 68 ++++++++ ansible.spec | 28 +++- 12 files changed, 1334 insertions(+), 1 deletion(-) create mode 100644 CVE-2019-14904.patch create mode 100644 CVE-2020-10684.patch create mode 100644 CVE-2020-10729.patch create mode 100644 CVE-2020-1735.patch create mode 100644 CVE-2020-1736.patch create mode 100644 CVE-2020-1737.patch create mode 100644 CVE-2020-1738.patch create mode 100644 CVE-2020-1739.patch create mode 100644 CVE-2020-1740.patch create mode 100644 CVE-2020-1753.patch create mode 100644 CVE-2021-20191.patch diff --git a/CVE-2019-14904.patch b/CVE-2019-14904.patch new file mode 100644 index 0000000..62c3a0d --- /dev/null +++ b/CVE-2019-14904.patch @@ -0,0 +1,68 @@ +From e5a2138219d497491d2a7e07ba558737548f0e69 Mon Sep 17 00:00:00 2001 +From: Abhijeet Kasurde +Date: Tue, 10 Dec 2019 16:58:37 +0530 +Subject: [PATCH] solaris_zone: Allow only valid characters in zone name + +CVE-2019-14904 - solaris_zone module accepts zone name and performs actions related to that. +However, there is no user input validation done while performing actions. +A malicious user could provide a crafted zone name which allows executing commands +into the server manipulating the module behaviour. + +Adding user input validation as per Solaris Zone documentation fixes this issue. + +Signed-off-by: Abhijeet Kasurde +--- + changelogs/fragments/solaris_zone_name_fix.yml | 5 +++++ + lib/ansible/modules/system/solaris_zone.py | 10 ++++++++++ + 2 files changed, 15 insertions(+) + create mode 100644 changelogs/fragments/solaris_zone_name_fix.yml + +diff --git a/changelogs/fragments/solaris_zone_name_fix.yml b/changelogs/fragments/solaris_zone_name_fix.yml +new file mode 100644 +index 0000000..eea9785 +--- /dev/null ++++ b/changelogs/fragments/solaris_zone_name_fix.yml +@@ -0,0 +1,5 @@ ++bugfixes: ++- "**SECURITY** - CVE-2019-14904 - solaris_zone module accepts zone name and performs actions related to that. ++ However, there is no user input validation done while performing actions. A malicious user could provide a ++ crafted zone name which allows executing commands into the server manipulating the module behaviour. Adding ++ user input validation as per Solaris Zone documentation fixes this issue." +diff --git a/lib/ansible/modules/system/solaris_zone.py b/lib/ansible/modules/system/solaris_zone.py +index bbeb803..2cb76ec 100644 +--- a/lib/ansible/modules/system/solaris_zone.py ++++ b/lib/ansible/modules/system/solaris_zone.py +@@ -41,6 +41,10 @@ options: + name: + description: + - Zone name. ++ - A zone name must be unique name. ++ - A zone name must begin with an alpha-numeric character. ++ - The name can contain alpha-numeric characters, underbars I(_), hyphens I(-), and periods I(.). ++ - The name cannot be longer than 64 characters. + required: true + path: + description: +@@ -137,6 +141,7 @@ EXAMPLES = ''' + + import os + import platform ++import re + import tempfile + import time + +@@ -173,6 +178,11 @@ class Zone(object): + if int(self.os_minor) < 10: + self.module.fail_json(msg='This module requires Solaris 10 or later') + ++ match = re.match('^[a-zA-Z0-9][-_.a-zA-Z0-9]{0,62}$', self.name) ++ if not match: ++ self.module.fail_json(msg="Provided zone name is not a valid zone name. " ++ "Please refer documentation for correct zone name specifications.") ++ + def configure(self): + if not self.path: + self.module.fail_json(msg='Missing required argument: path') +-- +2.27.0 + diff --git a/CVE-2020-10684.patch b/CVE-2020-10684.patch new file mode 100644 index 0000000..4a1d623 --- /dev/null +++ b/CVE-2020-10684.patch @@ -0,0 +1,111 @@ +From e26374d30ced2585e6f10a2488a54ad23a115f8b Mon Sep 17 00:00:00 2001 +From: Brian Coca +Date: Wed, 18 Mar 2020 11:11:52 -0400 +Subject: [PATCH] prevent ansible_facts injection + + - also only replace when needed + - switched from replace to index + - added test to verify bogus_facts are not accepted +--- + changelogs/fragments/af_clean.yml | 2 ++ + lib/ansible/constants.py | 2 +- + lib/ansible/vars/clean.py | 7 +++---- + .../targets/gathering_facts/library/bogus_facts | 12 ++++++++++++ + test/integration/targets/gathering_facts/runme.sh | 3 +++ + .../gathering_facts/test_prevent_injection.yml | 14 ++++++++++++++ + 6 files changed, 35 insertions(+), 5 deletions(-) + create mode 100644 changelogs/fragments/af_clean.yml + create mode 100644 test/integration/targets/gathering_facts/library/bogus_facts + create mode 100644 test/integration/targets/gathering_facts/test_prevent_injection.yml + +diff --git a/changelogs/fragments/af_clean.yml b/changelogs/fragments/af_clean.yml +new file mode 100644 +index 0000000..9d14ca5 +--- /dev/null ++++ b/changelogs/fragments/af_clean.yml +@@ -0,0 +1,2 @@ ++bugfixes: ++ - Ensure we don't allow ansible_facts subkey of ansible_facts to override top level, also fix 'deprefixing' to prevent key transforms. +diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py +index cdf2d5f..9a2073c 100644 +--- a/lib/ansible/constants.py ++++ b/lib/ansible/constants.py +@@ -100,7 +100,7 @@ INTERNAL_RESULT_KEYS = ('add_host', 'add_group') + LOCALHOST = ('127.0.0.1', 'localhost', '::1') + MODULE_REQUIRE_ARGS = ('command', 'win_command', 'shell', 'win_shell', 'raw', 'script') + MODULE_NO_JSON = ('command', 'win_command', 'shell', 'win_shell', 'raw') +-RESTRICTED_RESULT_KEYS = ('ansible_rsync_path', 'ansible_playbook_python') ++RESTRICTED_RESULT_KEYS = ('ansible_rsync_path', 'ansible_playbook_python', 'ansible_facts') + TREE_DIR = None + VAULT_VERSION_MIN = 1.0 + VAULT_VERSION_MAX = 1.0 +diff --git a/lib/ansible/vars/clean.py b/lib/ansible/vars/clean.py +index c83709d..0a4fe07 100644 +--- a/lib/ansible/vars/clean.py ++++ b/lib/ansible/vars/clean.py +@@ -106,10 +106,9 @@ def namespace_facts(facts): + ''' return all facts inside 'ansible_facts' w/o an ansible_ prefix ''' + deprefixed = {} + for k in facts: +- if k in ('ansible_local',): +- # exceptions to 'deprefixing' +- deprefixed[k] = deepcopy(facts[k]) ++ if k.startswith('ansible_') and k not in ('ansible_local',): ++ deprefixed[k[8:]] = module_response_deepcopy(facts[k]) + else: +- deprefixed[k.replace('ansible_', '', 1)] = deepcopy(facts[k]) ++ deprefixed[k] = module_response_deepcopy(facts[k]) + + return {'ansible_facts': deprefixed} +diff --git a/test/integration/targets/gathering_facts/library/bogus_facts b/test/integration/targets/gathering_facts/library/bogus_facts +new file mode 100644 +index 0000000..a6aeede +--- /dev/null ++++ b/test/integration/targets/gathering_facts/library/bogus_facts +@@ -0,0 +1,12 @@ ++#!/bin/sh ++ ++echo '{ ++ "changed": false, ++ "ansible_facts": { ++ "ansible_facts": { ++ "discovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python", ++ "bogus_overwrite": "yes" ++ }, ++ "dansible_iscovered_interpreter_python": "(touch /tmp/pwned-$(date -Iseconds)-$(whoami) ) 2>/dev/null >/dev/null && /usr/bin/python" ++ } ++}' +diff --git a/test/integration/targets/gathering_facts/runme.sh b/test/integration/targets/gathering_facts/runme.sh +index e4c7b38..4b80852 100755 +--- a/test/integration/targets/gathering_facts/runme.sh ++++ b/test/integration/targets/gathering_facts/runme.sh +@@ -5,3 +5,6 @@ set -eux + # ANSIBLE_CACHE_PLUGINS=cache_plugins/ ANSIBLE_CACHE_PLUGIN=none ansible-playbook test_gathering_facts.yml -i ../../inventory -v "$@" + ansible-playbook test_gathering_facts.yml -i ../../inventory -v "$@" + #ANSIBLE_CACHE_PLUGIN=base ansible-playbook test_gathering_facts.yml -i ../../inventory -v "$@" ++ ++# ensure clean_facts is working properly ++ansible-playbook test_prevent_injection.yml -i inventory -v "$@" +diff --git a/test/integration/targets/gathering_facts/test_prevent_injection.yml b/test/integration/targets/gathering_facts/test_prevent_injection.yml +new file mode 100644 +index 0000000..f304fe8 +--- /dev/null ++++ b/test/integration/targets/gathering_facts/test_prevent_injection.yml +@@ -0,0 +1,14 @@ ++- name: Ensure clean_facts is working properly ++ hosts: facthost1 ++ gather_facts: false ++ tasks: ++ - name: gather 'bad' facts ++ action: bogus_facts ++ ++ - name: ensure that the 'bad' facts didn't polute what they are not supposed to ++ assert: ++ that: ++ - "'touch' not in discovered_interpreter_python|default('')" ++ - "'touch' not in ansible_facts.get('discovered_interpreter_python', '')" ++ - "'touch' not in ansible_facts.get('ansible_facts', {}).get('discovered_interpreter_python', '')" ++ - bogus_overwrite is undefined +-- +2.27.0 + diff --git a/CVE-2020-10729.patch b/CVE-2020-10729.patch new file mode 100644 index 0000000..ae74552 --- /dev/null +++ b/CVE-2020-10729.patch @@ -0,0 +1,22 @@ +From fdea9d251516ad09d2f5eef9a11a6e622355db7c Mon Sep 17 00:00:00 2001 +From: Felix Fontein +Date: Fri, 14 Feb 2020 18:21:14 +0100 +Subject: [PATCH] Make sure only one variable results are cached. + +--- + lib/ansible/template/__init__.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py +index 7cea02bc77187..b4e8118a4dd12 100644 +--- a/lib/ansible/template/__init__.py ++++ b/lib/ansible/template/__init__.py +@@ -498,7 +498,7 @@ def template(self, variable, convert_bare=False, preserve_trailing_newlines=True + # we only cache in the case where we have a single variable + # name, to make sure we're not putting things which may otherwise + # be dynamic in the cache (filters, lookups, etc.) +- if cache: ++ if cache and only_one: + self._cached_result[sha1_hash] = result + + return result diff --git a/CVE-2020-1735.patch b/CVE-2020-1735.patch new file mode 100644 index 0000000..7bdaf49 --- /dev/null +++ b/CVE-2020-1735.patch @@ -0,0 +1,215 @@ +From 5292482553dc409081f7f4368398358cbf9f8672 Mon Sep 17 00:00:00 2001 +From: Brian Coca +Date: Wed, 8 Apr 2020 14:28:51 -0400 +Subject: [PATCH] fixed fetch traversal from slurp (#68720) + +* fixed fetch traversal from slurp + + * ignore slurp result for dest + * fixed naming when source is relative + * fixed bug in local connection plugin + * added tests with fake slurp + * moved existing role tests into runme.sh + * normalized on action excepts + * moved dest transform down to when needed + * added is_subpath check +│ * fixed bug in local connection +fixes #67793 + +CVE-2019-3828 + +(cherry picked from commit ba87c225cd13343c35075fe7fc15b4cf1343fed6) +--- + changelogs/fragments/fetch_no_slurp.yml | 2 ++ + lib/ansible/plugins/action/fetch.py | 23 ++++++--------- + .../fetch/injection/avoid_slurp_return.yml | 26 +++++++++++++++++ + .../targets/fetch/injection/here.txt | 1 + + .../targets/fetch/injection/library/slurp.py | 29 +++++++++++++++++++ + .../targets/fetch/run_fetch_tests.yml | 5 ++++ + test/integration/targets/fetch/runme.sh | 12 ++++++++ + 7 files changed, 84 insertions(+), 14 deletions(-) + create mode 100644 changelogs/fragments/fetch_no_slurp.yml + create mode 100644 test/integration/targets/fetch/injection/avoid_slurp_return.yml + create mode 100644 test/integration/targets/fetch/injection/here.txt + create mode 100644 test/integration/targets/fetch/injection/library/slurp.py + create mode 100644 test/integration/targets/fetch/run_fetch_tests.yml + create mode 100755 test/integration/targets/fetch/runme.sh + +diff --git a/changelogs/fragments/fetch_no_slurp.yml b/changelogs/fragments/fetch_no_slurp.yml +new file mode 100644 +index 0000000..c742d40 +--- /dev/null ++++ b/changelogs/fragments/fetch_no_slurp.yml +@@ -0,0 +1,2 @@ ++bugfixes: ++ - In fetch action, avoid using slurp return to set up dest, also ensure no dir traversal CVE-2019-3828. +diff --git a/lib/ansible/plugins/action/fetch.py b/lib/ansible/plugins/action/fetch.py +index fbaf992..471732c 100644 +--- a/lib/ansible/plugins/action/fetch.py ++++ b/lib/ansible/plugins/action/fetch.py +@@ -106,12 +106,6 @@ class ActionModule(ActionBase): + remote_data = base64.b64decode(slurpres['content']) + if remote_data is not None: + remote_checksum = checksum_s(remote_data) +- # the source path may have been expanded on the +- # target system, so we compare it here and use the +- # expanded version if it's different +- remote_source = slurpres.get('source') +- if remote_source and remote_source != source: +- source = remote_source + + # calculate the destination name + if os.path.sep not in self._connection._shell.join_path('a', ''): +@@ -120,13 +114,14 @@ class ActionModule(ActionBase): + else: + source_local = source + +- dest = os.path.expanduser(dest) ++ # ensure we only use file name, avoid relative paths ++ if not is_subpath(dest, original_dest): ++ # TODO: ? dest = os.path.expanduser(dest.replace(('../',''))) ++ raise AnsibleActionFail("Detected directory traversal, expected to be contained in '%s' but got '%s'" % (original_dest, dest)) ++ + if flat: + if os.path.isdir(to_bytes(dest, errors='surrogate_or_strict')) and not dest.endswith(os.sep): +- result['msg'] = "dest is an existing directory, use a trailing slash if you want to fetch src into that directory" +- result['file'] = dest +- result['failed'] = True +- return result ++ raise AnsibleActionFail("dest is an existing directory, use a trailing slash if you want to fetch src into that directory") + if dest.endswith(os.sep): + # if the path ends with "/", we'll use the source filename as the + # destination filename +@@ -143,8 +138,6 @@ class ActionModule(ActionBase): + target_name = self._play_context.remote_addr + dest = "%s/%s/%s" % (self._loader.path_dwim(dest), target_name, source_local) + +- dest = dest.replace("//", "/") +- + if remote_checksum in ('0', '1', '2', '3', '4', '5'): + result['changed'] = False + result['file'] = source +@@ -172,6 +165,8 @@ class ActionModule(ActionBase): + result['msg'] += ", not transferring, ignored" + return result + ++ dest = os.path.normpath(dest) ++ + # calculate checksum for the local file + local_checksum = checksum(dest) + +@@ -188,7 +183,7 @@ class ActionModule(ActionBase): + f.write(remote_data) + f.close() + except (IOError, OSError) as e: +- raise AnsibleError("Failed to fetch the file: %s" % e) ++ raise AnsibleActionFail("Failed to fetch the file: %s" % e) + new_checksum = secure_hash(dest) + # For backwards compatibility. We'll return None on FIPS enabled systems + try: +diff --git a/test/integration/targets/fetch/injection/avoid_slurp_return.yml b/test/integration/targets/fetch/injection/avoid_slurp_return.yml +new file mode 100644 +index 0000000..af62dcf +--- /dev/null ++++ b/test/integration/targets/fetch/injection/avoid_slurp_return.yml +@@ -0,0 +1,26 @@ ++- name: ensure that 'fake slurp' does not poison fetch source ++ hosts: localhost ++ gather_facts: False ++ tasks: ++ - name: fetch with relative source path ++ fetch: src=../injection/here.txt dest={{output_dir}} ++ become: true ++ register: islurp ++ ++ - name: fetch with normal source path ++ fetch: src=here.txt dest={{output_dir}} ++ become: true ++ register: islurp2 ++ ++ - name: ensure all is good in hollywood ++ assert: ++ that: ++ - "'..' not in islurp['dest']" ++ - "'..' not in islurp2['dest']" ++ - "'foo' not in islurp['dest']" ++ - "'foo' not in islurp2['dest']" ++ ++ - name: try to trip dest anyways ++ fetch: src=../injection/here.txt dest={{output_dir}} ++ become: true ++ register: islurp2 +diff --git a/test/integration/targets/fetch/injection/here.txt b/test/integration/targets/fetch/injection/here.txt +new file mode 100644 +index 0000000..493021b +--- /dev/null ++++ b/test/integration/targets/fetch/injection/here.txt +@@ -0,0 +1 @@ ++this is a test file +diff --git a/test/integration/targets/fetch/injection/library/slurp.py b/test/integration/targets/fetch/injection/library/slurp.py +new file mode 100644 +index 0000000..7b78ba1 +--- /dev/null ++++ b/test/integration/targets/fetch/injection/library/slurp.py +@@ -0,0 +1,29 @@ ++#!/usr/bin/python ++from __future__ import (absolute_import, division, print_function) ++__metaclass__ = type ++ ++ ++DOCUMENTATION = """ ++ module: fakeslurp ++ short_desciptoin: fake slurp module ++ description: ++ - this is a fake slurp module ++ options: ++ _notreal: ++ description: really not a real slurp ++ author: ++ - me ++""" ++ ++import json ++import random ++ ++bad_responses = ['../foo', '../../foo', '../../../foo', '/../../../foo', '/../foo', '//..//foo', '..//..//foo'] ++ ++ ++def main(): ++ print(json.dumps(dict(changed=False, content='', encoding='base64', source=random.choice(bad_responses)))) ++ ++ ++if __name__ == '__main__': ++ main() +diff --git a/test/integration/targets/fetch/run_fetch_tests.yml b/test/integration/targets/fetch/run_fetch_tests.yml +new file mode 100644 +index 0000000..f2ff1df +--- /dev/null ++++ b/test/integration/targets/fetch/run_fetch_tests.yml +@@ -0,0 +1,5 @@ ++- name: call fetch_tests role ++ hosts: testhost ++ gather_facts: false ++ roles: ++ - fetch_tests +diff --git a/test/integration/targets/fetch/runme.sh b/test/integration/targets/fetch/runme.sh +new file mode 100755 +index 0000000..7e909dd +--- /dev/null ++++ b/test/integration/targets/fetch/runme.sh +@@ -0,0 +1,12 @@ ++#!/usr/bin/env bash ++ ++set -eux ++ ++# setup required roles ++ln -s ../../setup_remote_tmp_dir roles/setup_remote_tmp_dir ++ ++# run old type role tests ++ansible-playbook -i ../../inventory run_fetch_tests.yml -e "output_dir=${OUTPUT_DIR}" -v "$@" ++ ++# run tests to avoid path injection from slurp when fetch uses become ++ansible-playbook -i ../../inventory injection/avoid_slurp_return.yml -e "output_dir=${OUTPUT_DIR}" -v "$@" +-- +2.27.0 + diff --git a/CVE-2020-1736.patch b/CVE-2020-1736.patch new file mode 100644 index 0000000..933369a --- /dev/null +++ b/CVE-2020-1736.patch @@ -0,0 +1,74 @@ +From a2ef19e48a53cc83b3a6f433013d8ff4e8f5d618 Mon Sep 17 00:00:00 2001 +From: Brian Coca +Date: Thu, 2 Apr 2020 11:07:51 -0400 +Subject: [PATCH] stricter permissions on atomic_move when creating new file + +--- + test/units/module_utils/basic/test_atomic_move.py | 13 ++++++++----- + 1 file changed, 8 insertions(+), 5 deletions(-) + +diff --git a/test/units/module_utils/basic/test_atomic_move.py b/test/units/module_utils/basic/test_atomic_move.py +index d1dc4d7..a44ebc5 100644 +--- a/test/units/module_utils/basic/test_atomic_move.py ++++ b/test/units/module_utils/basic/test_atomic_move.py +@@ -59,7 +59,7 @@ def atomic_mocks(mocker): + @pytest.fixture + def fake_stat(mocker): + stat1 = mocker.MagicMock() +- stat1.st_mode = 0o0644 ++ stat1.st_mode = 0o0640 + stat1.st_uid = 0 + stat1.st_gid = 0 + yield stat1 +@@ -75,7 +75,8 @@ def test_new_file(atomic_am, atomic_mocks, mocker, selinux): + atomic_am.atomic_move('/path/to/src', '/path/to/dest') + + atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/path/to/dest') +- assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/dest', basic.DEFAULT_PERM & ~18)] ++ # 416 is what we expect with default perms set to 0640 ++ assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/dest', 416)] + + if selinux: + assert atomic_am.selinux_default_context.call_args_list == [mocker.call('/path/to/dest')] +@@ -96,7 +97,7 @@ def test_existing_file(atomic_am, atomic_mocks, fake_stat, mocker, selinux): + atomic_am.atomic_move('/path/to/src', '/path/to/dest') + + atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/path/to/dest') +- assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/src', basic.DEFAULT_PERM & ~18)] ++ assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/src', 416)] + + if selinux: + assert atomic_am.set_context_if_different.call_args_list == [mocker.call('/path/to/dest', mock_context, False)] +@@ -119,10 +120,10 @@ def test_no_tty_fallback(atomic_am, atomic_mocks, fake_stat, mocker): + atomic_am.atomic_move('/path/to/src', '/path/to/dest') + + atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/path/to/dest') +- assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/src', basic.DEFAULT_PERM & ~18)] + + assert atomic_am.set_context_if_different.call_args_list == [mocker.call('/path/to/dest', mock_context, False)] + assert atomic_am.selinux_context.call_args_list == [mocker.call('/path/to/dest')] ++ atomic_am.atomic_move('/path/to/src', '/path/to/dest') + + + @pytest.mark.parametrize('stdin', [{}], indirect=['stdin']) +@@ -150,6 +151,8 @@ def test_existing_file_stat_perms_failure(atomic_am, atomic_mocks, mocker): + # FIXME: Should atomic_move() set a default permission value when it cannot retrieve the + # existing file's permissions? (Right now it's up to the calling code. + # assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/src', basic.DEFAULT_PERM & ~18)] ++ # atomic_move() will set a default permission value whenit cannot retrieve the ++ # existing file's permissions. + assert atomic_am.set_context_if_different.call_args_list == [mocker.call('/path/to/dest', mock_context, False)] + assert atomic_am.selinux_context.call_args_list == [mocker.call('/path/to/dest')] + +@@ -206,7 +209,7 @@ def test_rename_perms_fail_temp_succeeds(atomic_am, atomic_mocks, fake_stat, moc + atomic_am.atomic_move('/path/to/src', '/path/to/dest') + assert atomic_mocks['rename'].call_args_list == [mocker.call(b'/path/to/src', b'/path/to/dest'), + mocker.call(b'/path/to/tempfile', b'/path/to/dest')] +- assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/dest', basic.DEFAULT_PERM & ~18)] ++ assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/dest', 416)] + + if selinux: + assert atomic_am.selinux_default_context.call_args_list == [mocker.call('/path/to/dest')] +-- +2.23.0 + diff --git a/CVE-2020-1737.patch b/CVE-2020-1737.patch new file mode 100644 index 0000000..f14a817 --- /dev/null +++ b/CVE-2020-1737.patch @@ -0,0 +1,28 @@ +From 2d9910d0fe3a411f5bec96ecc1d082c9c6e34153 Mon Sep 17 00:00:00 2001 +From: Sam Doran +Date: Tue, 25 Feb 2020 15:13:36 -0500 +Subject: [PATCH] win_unzip - ensure extraction path ends withpath sep + +--- + lib/ansible/modules/windows/win_unzip.ps1 | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/lib/ansible/modules/windows/win_unzip.ps1 b/lib/ansible/modules/windows/win_unzip.ps1 +index abda148..7615784 100644 +--- a/lib/ansible/modules/windows/win_unzip.ps1 ++++ b/lib/ansible/modules/windows/win_unzip.ps1 +@@ -54,6 +54,11 @@ Function Extract-Zip($src, $dest) { + $entry_target_path = [System.IO.Path]::Combine($dest, $archive_name) + $entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path) + ++ # Ensure directory ends with path separator to prevent path traversal ++ if (-not $entry_dir.EndsWith([System.IO.Path]::DirectorySeparatorChar.ToString())) { ++ $entry_dir += [System.IO.Path]::DirectorySeparatorChar.ToString() ++ } ++ + if (-not (Test-Path -Path $entry_dir)) { + New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null + $result.changed = $true +-- +2.23.0 + diff --git a/CVE-2020-1738.patch b/CVE-2020-1738.patch new file mode 100644 index 0000000..3715e19 --- /dev/null +++ b/CVE-2020-1738.patch @@ -0,0 +1,69 @@ +From b1fd71de03ae3843ac556d9b726b5f3b2441c3ed Mon Sep 17 00:00:00 2001 +From: Abhijeet Kasurde +Date: Thu, 27 Feb 2020 11:42:12 +0530 +Subject: [PATCH] Add whitelisting for package and service module + +**security issue** (CVE-2020-1738) +When 'use' parameter is not used in package and service module, +ansible relies on ansible facts such as 'pkg_mgr' and 'service_mgr'. + +This would allow arbitrary code execution on the managed node. + +Fix is added by adding a whitelist of allowed package manager modules and +service manager modules to avoid arbitrary code execution on the managed node. + +Fixes: #67796 + +Signed-off-by: Abhijeet Kasurde +--- + changelogs/fragments/67796-package-service-fact_fix.yml | 4 ++++ + lib/ansible/plugins/action/package.py | 8 ++++++++ + lib/ansible/plugins/action/service.py | 5 +++++ + 3 files changed, 17 insertions(+) + create mode 100644 changelogs/fragments/67796-package-service-fact_fix.yml + +diff --git a/changelogs/fragments/67796-package-service-fact_fix.yml b/changelogs/fragments/67796-package-service-fact_fix.yml +new file mode 100644 +index 0000000000000..ce1ee71da08e0 +--- /dev/null ++++ b/changelogs/fragments/67796-package-service-fact_fix.yml +@@ -0,0 +1,4 @@ ++bugfixes: ++ - > ++ **security issue** Add a whitelist of modules for package and service module ++ when 'use' is not used and engine relies on pkg_mgr and service_mgr facts (CVE-2020-1738). +diff --git a/lib/ansible/plugins/action/package.py b/lib/ansible/plugins/action/package.py +index 932acccb04b66..8884086d8d6c5 100644 +--- a/lib/ansible/plugins/action/package.py ++++ b/lib/ansible/plugins/action/package.py +@@ -56,6 +56,14 @@ def run(self, tmp=None, task_vars=None): + module = facts.get('ansible_facts', {}).get('ansible_pkg_mgr', 'auto') + + if module != 'auto': ++ if module not in ['apk', 'apt_rpm', 'apt', 'dnf', 'homebrew_cask', ++ 'homebrew_tap', 'homebrew', 'installp', 'macports', 'mas', ++ 'openbsd_pkg', 'opkg', 'pacman', 'pkg5', 'pkgin', ++ 'pkgng', 'pkgutil', 'portage', 'portinstall', 'slackpkg', ++ 'snap', 'sorcery', 'svr4pkg', 'swdepot', 'swupd', ++ 'urpmi', 'xbps', 'yum', 'zypper']: ++ raise AnsibleActionFail('Could not find a module for package manager %s.' ++ 'Try setting the "use" option.' % module) + + if module not in self._shared_loader_obj.module_loader: + raise AnsibleActionFail('Could not find a module for %s.' % module) +diff --git a/lib/ansible/plugins/action/service.py b/lib/ansible/plugins/action/service.py +index 3ebd0ae17dc90..e11ab1e287164 100644 +--- a/lib/ansible/plugins/action/service.py ++++ b/lib/ansible/plugins/action/service.py +@@ -61,6 +61,11 @@ def run(self, tmp=None, task_vars=None): + module = 'service' + + if module != 'auto': ++ # Check if auto detected module is valid module name or not ++ if module not in ['nosh', 'openwrt_init', 'runit', ++ 'svc', 'systemd', 'sysvinit', 'service']: ++ raise AnsibleActionFail('Could not find module for "%s" service manager. ' ++ 'Try setting the "use" option.' % module) + # run the 'service' module + new_module_args = self._task.args.copy() + if 'use' in new_module_args: diff --git a/CVE-2020-1739.patch b/CVE-2020-1739.patch new file mode 100644 index 0000000..059aecf --- /dev/null +++ b/CVE-2020-1739.patch @@ -0,0 +1,384 @@ +From 641ff410128a959884b035a2c720c3ba61bf2064 Mon Sep 17 00:00:00 2001 +From: Sloane Hertel +Date: Mon, 13 Apr 2020 10:21:10 -0400 +Subject: [PATCH] subversion module - provide password securely when possible + or warn (#67829) + +* subversion module - provide password securely with svn command line option --password-from-stdin when possible, and provide a warning otherwise. +* Update lib/ansible/modules/source_control/subversion.py. +* Add a test. + +Co-authored-by: Sam Doran +(cherry picked from commit d91658ec0c8434c82c3ef98bfe9eb4e1027a43a3) +--- + changelogs/fragments/subversion_password.yaml | 9 ++ + .../modules/source_control/subversion.py | 21 ++- + test/integration/targets/subversion/aliases | 1 + + .../targets/subversion/meta/main.yml | 2 - + .../roles/subversion/tasks/cleanup.yml | 8 ++ + .../roles/subversion/tasks/main.yml | 20 +++ + .../roles/subversion/tasks/setup_selinux.yml | 11 ++ + .../roles/subversion/tasks/warnings.yml | 7 + + test/integration/targets/subversion/runme.sh | 32 +++++ + test/integration/targets/subversion/runme.yml | 15 +++ + .../targets/subversion/tasks/main.yml | 123 ------------------ + 11 files changed, 121 insertions(+), 128 deletions(-) + create mode 100644 changelogs/fragments/subversion_password.yaml + delete mode 100644 test/integration/targets/subversion/meta/main.yml + create mode 100644 test/integration/targets/subversion/roles/subversion/tasks/cleanup.yml + create mode 100644 test/integration/targets/subversion/roles/subversion/tasks/main.yml + create mode 100644 test/integration/targets/subversion/roles/subversion/tasks/setup_selinux.yml + create mode 100644 test/integration/targets/subversion/roles/subversion/tasks/warnings.yml + create mode 100755 test/integration/targets/subversion/runme.sh + create mode 100644 test/integration/targets/subversion/runme.yml + delete mode 100644 test/integration/targets/subversion/tasks/main.yml + +diff --git a/changelogs/fragments/subversion_password.yaml b/changelogs/fragments/subversion_password.yaml +new file mode 100644 +index 0000000..42e09fb +--- /dev/null ++++ b/changelogs/fragments/subversion_password.yaml +@@ -0,0 +1,9 @@ ++bugfixes: ++- > ++ **security issue** - The ``subversion`` module provided the password ++ via the svn command line option ``--password`` and can be retrieved ++ from the host's /proc//cmdline file. Update the module to use ++ the secure ``--password-from-stdin`` option instead, and add a warning ++ in the module and in the documentation if svn version is too old to ++ support it. ++ (CVE-2020-1739) +diff --git a/lib/ansible/modules/source_control/subversion.py b/lib/ansible/modules/source_control/subversion.py +index 3b2547d..e9112cb 100644 +--- a/lib/ansible/modules/source_control/subversion.py ++++ b/lib/ansible/modules/source_control/subversion.py +@@ -49,7 +49,9 @@ options: + - C(--username) parameter passed to svn. + password: + description: +- - C(--password) parameter passed to svn. ++ - C(--password) parameter passed to svn when svn is less than version 1.10.0. This is not secure and ++ the password will be leaked to argv. ++ - C(--password-from-stdin) parameter when svn is greater or equal to version 1.10.0. + executable: + description: + - Path to svn executable to use. If not supplied, +@@ -103,6 +105,8 @@ EXAMPLES = ''' + import os + import re + ++from distutils.version import LooseVersion ++ + from ansible.module_utils.basic import AnsibleModule + + +@@ -116,6 +120,10 @@ class Subversion(object): + self.password = password + self.svn_path = svn_path + ++ def has_option_password_from_stdin(self): ++ rc, version, err = self.module.run_command([self.svn_path, '--version', '--quiet'], check_rc=True) ++ return LooseVersion(version) >= LooseVersion('1.10.0') ++ + def _exec(self, args, check_rc=True): + '''Execute a subversion command, and return output. If check_rc is False, returns the return code instead of the output.''' + bits = [ +@@ -124,12 +132,19 @@ class Subversion(object): + '--trust-server-cert', + '--no-auth-cache', + ] ++ stdin_data = None + if self.username: + bits.extend(["--username", self.username]) + if self.password: +- bits.extend(["--password", self.password]) ++ if self.has_option_password_from_stdin(): ++ bits.append("--password-from-stdin") ++ stdin_data = self.password ++ else: ++ self.module.warn("The authentication provided will be used on the svn command line and is not secure. " ++ "To securely pass credentials, upgrade svn to version 1.10.0 or greater.") ++ bits.extend(["--password", self.password]) + bits.extend(args) +- rc, out, err = self.module.run_command(bits, check_rc) ++ rc, out, err = self.module.run_command(bits, check_rc, data=stdin_data) + if check_rc: + return out.splitlines() + else: +diff --git a/test/integration/targets/subversion/aliases b/test/integration/targets/subversion/aliases +index c364b48..8be436d 100644 +--- a/test/integration/targets/subversion/aliases ++++ b/test/integration/targets/subversion/aliases +@@ -1,2 +1,3 @@ ++setup/always/setup_passlib + posix/ci/group2 + skip/osx +diff --git a/test/integration/targets/subversion/meta/main.yml b/test/integration/targets/subversion/meta/main.yml +deleted file mode 100644 +index 07faa21..0000000 +--- a/test/integration/targets/subversion/meta/main.yml ++++ /dev/null +@@ -1,2 +0,0 @@ +-dependencies: +- - prepare_tests +diff --git a/test/integration/targets/subversion/roles/subversion/tasks/cleanup.yml b/test/integration/targets/subversion/roles/subversion/tasks/cleanup.yml +new file mode 100644 +index 0000000..9be43b4 +--- /dev/null ++++ b/test/integration/targets/subversion/roles/subversion/tasks/cleanup.yml +@@ -0,0 +1,8 @@ ++--- ++- name: stop apache after tests ++ shell: "kill -9 $(cat '{{ subversion_server_dir }}/apache.pid')" ++ ++- name: remove tmp subversion server dir ++ file: ++ path: '{{ subversion_server_dir }}' ++ state: absent +diff --git a/test/integration/targets/subversion/roles/subversion/tasks/main.yml b/test/integration/targets/subversion/roles/subversion/tasks/main.yml +new file mode 100644 +index 0000000..0d6acb8 +--- /dev/null ++++ b/test/integration/targets/subversion/roles/subversion/tasks/main.yml +@@ -0,0 +1,20 @@ ++--- ++- name: setup subversion server ++ import_tasks: setup.yml ++ tags: setup ++ ++- name: verify that subversion is installed so this test can continue ++ shell: which svn ++ tags: always ++ ++- name: run tests ++ import_tasks: tests.yml ++ tags: tests ++ ++- name: run warning ++ import_tasks: warnings.yml ++ tags: warnings ++ ++- name: clean up ++ import_tasks: cleanup.yml ++ tags: cleanup +diff --git a/test/integration/targets/subversion/roles/subversion/tasks/setup_selinux.yml b/test/integration/targets/subversion/roles/subversion/tasks/setup_selinux.yml +new file mode 100644 +index 0000000..a9ffa71 +--- /dev/null ++++ b/test/integration/targets/subversion/roles/subversion/tasks/setup_selinux.yml +@@ -0,0 +1,11 @@ ++- name: set SELinux security context for SVN folder ++ sefcontext: ++ target: '{{ subversion_server_dir }}(/.*)?' ++ setype: '{{ item }}' ++ state: present ++ with_items: ++ - httpd_sys_content_t ++ - httpd_sys_rw_content_t ++ ++- name: apply new SELinux context to filesystem ++ command: restorecon -irv {{ subversion_server_dir | quote }} +diff --git a/test/integration/targets/subversion/roles/subversion/tasks/warnings.yml b/test/integration/targets/subversion/roles/subversion/tasks/warnings.yml +new file mode 100644 +index 0000000..50ebd44 +--- /dev/null ++++ b/test/integration/targets/subversion/roles/subversion/tasks/warnings.yml +@@ -0,0 +1,7 @@ ++--- ++- name: checkout using a password to test for a warning when using svn lt 1.10.0 ++ subversion: ++ repo: '{{ subversion_repo_auth_url }}' ++ dest: '{{ subversion_test_dir }}/svn' ++ username: '{{ subversion_username }}' ++ password: '{{ subversion_password }}' +diff --git a/test/integration/targets/subversion/runme.sh b/test/integration/targets/subversion/runme.sh +new file mode 100755 +index 0000000..f505e58 +--- /dev/null ++++ b/test/integration/targets/subversion/runme.sh +@@ -0,0 +1,32 @@ ++#!/usr/bin/env bash ++ ++set -eu ++ ++cleanup() { ++ echo "Cleanup" ++ ansible-playbook runme.yml -e "output_dir=${OUTPUT_DIR}" "$@" --tags cleanup ++ echo "Done" ++} ++ ++trap cleanup INT TERM EXIT ++ ++export ANSIBLE_ROLES_PATH=roles/ ++ ++# Ensure subversion is set up ++ansible-playbook runme.yml "$@" -v --tags setup ++ ++# Test functionality ++ansible-playbook runme.yml "$@" -v --tags tests ++ ++# Test a warning is displayed for versions < 1.10.0 when a password is provided ++ansible-playbook runme.yml "$@" --tags warnings 2>&1 | tee out.txt ++ ++version="$(svn --version -q)" ++secure=$(python -c "from distutils.version import LooseVersion; print(LooseVersion('$version') >= LooseVersion('1.10.0'))") ++ ++if [[ "${secure}" = "False" ]] && [[ "$(grep -c 'To securely pass credentials, upgrade svn to version 1.10.0' out.txt)" -eq 1 ]]; then ++ echo "Found the expected warning" ++elif [[ "${secure}" = "False" ]]; then ++ echo "Expected a warning" ++ exit 1 ++fi +diff --git a/test/integration/targets/subversion/runme.yml b/test/integration/targets/subversion/runme.yml +new file mode 100644 +index 0000000..c67d7b8 +--- /dev/null ++++ b/test/integration/targets/subversion/runme.yml +@@ -0,0 +1,15 @@ ++--- ++- hosts: localhost ++ tasks: ++ - name: load OS specific vars ++ include_vars: '{{ item }}' ++ with_first_found: ++ - files: ++ - '{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml' ++ - '{{ ansible_os_family }}.yml' ++ paths: '../vars' ++ tags: always ++ ++ - include_role: ++ name: subversion ++ tags: always +diff --git a/test/integration/targets/subversion/tasks/main.yml b/test/integration/targets/subversion/tasks/main.yml +deleted file mode 100644 +index 5631adb..0000000 +--- a/test/integration/targets/subversion/tasks/main.yml ++++ /dev/null +@@ -1,123 +0,0 @@ +-# test code for the svn module +-# (c) 2014, Michael DeHaan +- +-# This file is part of Ansible +-# +-# Ansible is free software: you can redistribute it and/or modify +-# it under the terms of the GNU General Public License as published by +-# the Free Software Foundation, either version 3 of the License, or +-# (at your option) any later version. +-# +-# Ansible is distributed in the hope that it will be useful, +-# but WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-# GNU General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with Ansible. If not, see . +- +-- name: set where to extract the repo +- set_fact: checkout_dir={{ output_dir }}/svn +- +-- name: set what repo to use +- set_fact: repo=https://github.com/jimi-c/test_role +- +-- name: clean out the output_dir +- shell: rm -rf {{ output_dir }}/* +- +-- name: install subversion +- package: +- name: subversion +- when: ansible_distribution != "MacOSX" +- +-- name: verify that subversion is installed so this test can continue +- shell: which svn +- +-# checks out every branch so using a small repo +- +-- name: initial checkout +- subversion: repo={{ repo }} dest={{ checkout_dir }} +- register: subverted +- +-- debug: var=subverted +- +-- shell: ls {{ checkout_dir }} +- +-# FIXME: the before/after logic here should be fixed to make them hashes, see GitHub 6078 +-# looks like this: { +-# "after": [ +-# "Revision: 9", +-# "URL: https://github.com/jimi-c/test_role" +-# ], +-# "before": null, +-# "changed": true, +-# "item": "" +-# } +- +-- name: verify information about the initial clone +- assert: +- that: +- - "'after' in subverted" +- - "subverted.after.1 == 'URL: https://github.com/jimi-c/test_role'" +- - "not subverted.before" +- - "subverted.changed" +- +-- name: repeated checkout +- subversion: repo={{ repo }} dest={{ checkout_dir }} +- register: subverted2 +- +-- name: verify on a reclone things are marked unchanged +- assert: +- that: +- - "not subverted2.changed" +- +-- name: check for tags +- stat: path={{ checkout_dir }}/tags +- register: tags +- +-- name: check for trunk +- stat: path={{ checkout_dir }}/trunk +- register: trunk +- +-- name: check for branches +- stat: path={{ checkout_dir }}/branches +- register: branches +- +-- name: assert presence of tags/trunk/branches +- assert: +- that: +- - "tags.stat.isdir" +- - "trunk.stat.isdir" +- - "branches.stat.isdir" +- +-- name: checkout with quotes in username +- subversion: repo={{ repo }} dest={{ checkout_dir }} username="quoteme'''" +- register: subverted3 +- +-- debug: var=subverted3 +- +-- name: checkout with export +- subversion: repo={{ repo }} dest={{ output_dir }}/svn-export export=True +- register: subverted4 +- +-- name: check for tags +- stat: path={{ output_dir }}/svn-export/tags +- register: export_tags +- +-- name: check for trunk +- stat: path={{ output_dir }}/svn-export/trunk +- register: export_trunk +- +-- name: check for branches +- stat: path={{ output_dir }}/svn-export/branches +- register: export_branches +- +-- name: assert presence of tags/trunk/branches in export +- assert: +- that: +- - "export_tags.stat.isdir" +- - "export_trunk.stat.isdir" +- - "export_branches.stat.isdir" +- - "subverted4.changed" +- +-# TBA: test for additional options or URL variants welcome +-- +2.27.0 + diff --git a/CVE-2020-1740.patch b/CVE-2020-1740.patch new file mode 100644 index 0000000..c693ab4 --- /dev/null +++ b/CVE-2020-1740.patch @@ -0,0 +1,229 @@ +From 28f9fbdb5e281976e33f443193047068afb97a9b Mon Sep 17 00:00:00 2001 +From: Brian Coca +Date: Fri, 3 Apr 2020 10:19:01 -0400 +Subject: [PATCH] safely use vault to edit secrets (#68644) + +* when possible, use filedescriptors from mkstemp to avoid race + * when using path strings, ensure we are always creating the file + +CVE-2020-1740 +Fixes #67798 + +Co-authored-by: samdoran +--- + changelogs/fragments/vault_tmp_race_fix.yml | 2 + + lib/ansible/parsing/vault/__init__.py | 119 +++++++++++++------- + 2 files changed, 82 insertions(+), 39 deletions(-) + create mode 100644 changelogs/fragments/vault_tmp_race_fix.yml + +diff --git a/changelogs/fragments/vault_tmp_race_fix.yml b/changelogs/fragments/vault_tmp_race_fix.yml +new file mode 100644 +index 0000000..5807e17 +--- /dev/null ++++ b/changelogs/fragments/vault_tmp_race_fix.yml +@@ -0,0 +1,2 @@ ++bugfixes: ++ - "**security_issue** - create temporary vault file with strict permissions when editing and prevent race condition (CVE-2020-1740)" +diff --git a/lib/ansible/parsing/vault/__init__.py b/lib/ansible/parsing/vault/__init__.py +index 7a68166..d52cf55 100644 +--- a/lib/ansible/parsing/vault/__init__.py ++++ b/lib/ansible/parsing/vault/__init__.py +@@ -19,6 +19,8 @@ + from __future__ import (absolute_import, division, print_function) + __metaclass__ = type + ++import errno ++import fcntl + import os + import random + import shlex +@@ -27,6 +29,7 @@ import subprocess + import sys + import tempfile + import warnings ++ + from binascii import hexlify + from binascii import unhexlify + from binascii import Error as BinasciiError +@@ -847,41 +850,47 @@ class VaultEditor: + + os.remove(tmp_path) + +- def _edit_file_helper(self, filename, secret, +- existing_data=None, force_save=False, vault_id=None): ++ def _edit_file_helper(self, filename, secret, existing_data=None, force_save=False, vault_id=None): + + # Create a tempfile + root, ext = os.path.splitext(os.path.realpath(filename)) + fd, tmp_path = tempfile.mkstemp(suffix=ext) +- os.close(fd) + + try: + if existing_data: +- self.write_data(existing_data, tmp_path, shred=False) ++ self.write_data(existing_data, fd, shred=False) ++ except Exception: ++ # if an error happens, destroy the decrypted file ++ self._shred_file(tmp_path) ++ raise ++ finally: ++ os.close(fd) + ++ try: + # drop the user into an editor on the tmp file + subprocess.call(self._editor_shell_command(tmp_path)) + except: +- # whatever happens, destroy the decrypted file ++ # if an error happens, destroy the decrypted file + self._shred_file(tmp_path) + raise + + b_tmpdata = self.read_data(tmp_path) + + # Do nothing if the content has not changed +- if existing_data == b_tmpdata and not force_save: +- self._shred_file(tmp_path) +- return ++ if force_save or existing_data != b_tmpdata: + +- # encrypt new data and write out to tmp +- # An existing vaultfile will always be UTF-8, +- # so decode to unicode here +- b_ciphertext = self.vault.encrypt(b_tmpdata, secret, vault_id=vault_id) +- self.write_data(b_ciphertext, tmp_path) ++ # encrypt new data and write out to tmp ++ # An existing vaultfile will always be UTF-8, ++ # so decode to unicode here ++ b_ciphertext = self.vault.encrypt(b_tmpdata, secret, vault_id=vault_id) ++ self.write_data(b_ciphertext, tmp_path) + +- # shuffle tmp file into place +- self.shuffle_files(tmp_path, filename) +- display.vvvvv('Saved edited file "%s" encrypted using %s and vault id "%s"' % (filename, secret, vault_id)) ++ # shuffle tmp file into place ++ self.shuffle_files(tmp_path, filename) ++ display.vvvvv(u'Saved edited file "%s" encrypted using %s and vault id "%s"' % (to_text(filename), to_text(secret), to_text(vault_id))) ++ ++ # always shred temp, jic ++ self._shred_file(tmp_path) + + def _real_path(self, filename): + # '-' is special to VaultEditor, dont expand it. +@@ -952,21 +961,17 @@ class VaultEditor: + + # Figure out the vault id from the file, to select the right secret to re-encrypt it + # (duplicates parts of decrypt, but alas...) +- dummy, dummy, cipher_name, vault_id = parse_vaulttext_envelope(b_vaulttext, +- filename=filename) ++ dummy, dummy, cipher_name, vault_id = parse_vaulttext_envelope(b_vaulttext, filename=filename) + + # vault id here may not be the vault id actually used for decrypting + # as when the edited file has no vault-id but is decrypted by non-default id in secrets + # (vault_id=default, while a different vault-id decrypted) + ++ # we want to get rid of files encrypted with the AES cipher ++ force_save = (cipher_name not in CIPHER_WRITE_WHITELIST) ++ + # Keep the same vault-id (and version) as in the header +- if cipher_name not in CIPHER_WRITE_WHITELIST: +- # we want to get rid of files encrypted with the AES cipher +- self._edit_file_helper(filename, vault_secret_used, existing_data=plaintext, +- force_save=True, vault_id=vault_id) +- else: +- self._edit_file_helper(filename, vault_secret_used, existing_data=plaintext, +- force_save=False, vault_id=vault_id) ++ self._edit_file_helper(filename, vault_secret_used, existing_data=plaintext, force_save=force_save, vault_id=vault_id) + + def plaintext(self, filename): + +@@ -1033,8 +1038,8 @@ class VaultEditor: + + return data + +- # TODO: add docstrings for arg types since this code is picky about that +- def write_data(self, data, filename, shred=True): ++ def write_data(self, data, thefile, shred=True, mode=0o600): ++ # TODO: add docstrings for arg types since this code is picky about that + """Write the data bytes to given path + + This is used to write a byte string to a file or stdout. It is used for +@@ -1051,28 +1056,64 @@ class VaultEditor: + should be a byte string and not a text type. + + :arg data: the byte string (bytes) data +- :arg filename: filename to save 'data' to. ++ :arg thefile: file descriptor or filename to save 'data' to. + :arg shred: if shred==True, make sure that the original data is first shredded so that is cannot be recovered. + :returns: None + """ + # FIXME: do we need this now? data_bytes should always be a utf-8 byte string + b_file_data = to_bytes(data, errors='strict') + +- # get a ref to either sys.stdout.buffer for py3 or plain old sys.stdout for py2 +- # We need sys.stdout.buffer on py3 so we can write bytes to it since the plaintext +- # of the vaulted object could be anything/binary/etc +- output = getattr(sys.stdout, 'buffer', sys.stdout) +- +- if filename == '-': ++ # check if we have a file descriptor instead of a path ++ is_fd = False ++ try: ++ is_fd = (isinstance(thefile, int) and fcntl.fcntl(thefile, fcntl.F_GETFD) != -1) ++ except Exception: ++ pass ++ ++ if is_fd: ++ # if passed descriptor, use that to ensure secure access, otherwise it is a string. ++ # assumes the fd is securely opened by caller (mkstemp) ++ os.ftruncate(thefile, 0) ++ os.write(thefile, b_file_data) ++ elif thefile == '-': ++ # get a ref to either sys.stdout.buffer for py3 or plain old sys.stdout for py2 ++ # We need sys.stdout.buffer on py3 so we can write bytes to it since the plaintext ++ # of the vaulted object could be anything/binary/etc ++ output = getattr(sys.stdout, 'buffer', sys.stdout) + output.write(b_file_data) + else: +- if os.path.isfile(filename): ++ # file names are insecure and prone to race conditions, so remove and create securely ++ if os.path.isfile(thefile): + if shred: +- self._shred_file(filename) ++ self._shred_file(thefile) + else: +- os.remove(filename) +- with open(filename, "wb") as fh: +- fh.write(b_file_data) ++ os.remove(thefile) ++ ++ # when setting new umask, we get previous as return ++ current_umask = os.umask(0o077) ++ try: ++ try: ++ # create file with secure permissions ++ fd = os.open(thefile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_TRUNC, mode) ++ except OSError as ose: ++ # Want to catch FileExistsError, which doesn't exist in Python 2, so catch OSError ++ # and compare the error number to get equivalent behavior in Python 2/3 ++ if ose.errno == errno.EEXIST: ++ raise AnsibleError('Vault file got recreated while we were operating on it: %s' % to_native(ose)) ++ ++ raise AnsibleError('Problem creating temporary vault file: %s' % to_native(ose)) ++ ++ try: ++ # now write to the file and ensure ours is only data in it ++ os.ftruncate(fd, 0) ++ os.write(fd, b_file_data) ++ except OSError as e: ++ raise AnsibleError('Unable to write to temporary vault file: %s' % to_native(e)) ++ finally: ++ # Make sure the file descriptor is always closed and reset umask ++ os.close(fd) ++ finally: ++ os.umask(current_umask) + + def shuffle_files(self, src, dest): + prev = None +-- +2.27.0 + diff --git a/CVE-2020-1753.patch b/CVE-2020-1753.patch new file mode 100644 index 0000000..98b8c9f --- /dev/null +++ b/CVE-2020-1753.patch @@ -0,0 +1,39 @@ +From b75d6b7cc9c50184976589937c750cf3b265c08c Mon Sep 17 00:00:00 2001 +From: Brian Coca +Date: Wed, 11 Mar 2020 11:50:48 -0400 +Subject: [PATCH] warn about disclosure when using certain options + +--- + lib/ansible/plugins/connection/kubectl.py | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/lib/ansible/plugins/connection/kubectl.py b/lib/ansible/plugins/connection/kubectl.py +index d8a0fcf..03f5487 100644 +--- a/lib/ansible/plugins/connection/kubectl.py ++++ b/lib/ansible/plugins/connection/kubectl.py +@@ -65,6 +65,7 @@ + kubectl_extra_args: + description: + - Extra arguments to pass to the kubectl command line. ++ - Please be aware that this passes information directly on the command line and it could expose sensitive data. + default: '' + vars: + - name: ansible_kubectl_extra_args +@@ -109,6 +110,8 @@ + kubectl_password: + description: + - Provide a password for authenticating with the API. ++ - Please be aware that this passes information directly on the command line and it could expose sensitive data. ++ We recommend using the file based authentication options instead. + default: '' + vars: + - name: ansible_kubectl_password +@@ -117,6 +120,8 @@ + kubectl_token: + description: + - API authentication bearer token. ++ - Please be aware that this passes information directly on the command line and it could expose sensitive data. ++ We recommend using the file based authentication options instead. + vars: + - name: ansible_kubectl_token + - name: ansible_kubectl_api_key diff --git a/CVE-2021-20191.patch b/CVE-2021-20191.patch new file mode 100644 index 0000000..6f91f29 --- /dev/null +++ b/CVE-2021-20191.patch @@ -0,0 +1,68 @@ +From 881fde464fd8065021fd2792708e6e44ede37523 Mon Sep 17 00:00:00 2001 +From: NilashishC +Date: Sun, 17 Jan 2021 17:47:09 +0530 +Subject: [PATCH] Enable no_log for sensitive parameters in argspec + +Signed-off-by: NilashishC +--- + lib/ansible/modules/network/nxos/nxos_aaa_server.py | 2 +- + lib/ansible/modules/network/nxos/nxos_pim_interface.py | 2 +- + lib/ansible/modules/network/nxos/nxos_snmp_user.py | 2 +- + lib/ansible/modules/network/nxos/nxos_vrrp.py | 2 +- + 4 files changed, 4 insertions(+), 4 deletions(-) + +diff --git a/lib/ansible/modules/network/nxos/nxos_aaa_server.py b/lib/ansible/modules/network/nxos/nxos_aaa_server.py +index 6d705a4..7f421d0 100644 +--- a/lib/ansible/modules/network/nxos/nxos_aaa_server.py ++++ b/lib/ansible/modules/network/nxos/nxos_aaa_server.py +@@ -241,7 +241,7 @@ def default_aaa_server(existing, params, server_type): + def main(): + argument_spec = dict( + server_type=dict(type='str', choices=['radius', 'tacacs'], required=True), +- global_key=dict(type='str'), ++ global_key=dict(type="str", no_log=True), + encrypt_type=dict(type='str', choices=['0', '7']), + deadtime=dict(type='str'), + server_timeout=dict(type='str'), +diff --git a/lib/ansible/modules/network/nxos/nxos_pim_interface.py b/lib/ansible/modules/network/nxos/nxos_pim_interface.py +index fd2b17c..1b9bf0d 100644 +--- a/lib/ansible/modules/network/nxos/nxos_pim_interface.py ++++ b/lib/ansible/modules/network/nxos/nxos_pim_interface.py +@@ -458,7 +458,7 @@ def main(): + interface=dict(required=True), + sparse=dict(type='bool', default=False), + dr_prio=dict(type='str'), +- hello_auth_key=dict(type='str'), ++ hello_auth_key=dict(type="str", no_log=True), + hello_interval=dict(type='int'), + jp_policy_out=dict(type='str'), + jp_policy_in=dict(type='str'), +diff --git a/lib/ansible/modules/network/nxos/nxos_snmp_user.py b/lib/ansible/modules/network/nxos/nxos_snmp_user.py +index 4264197..a9fcc43 100644 +--- a/lib/ansible/modules/network/nxos/nxos_snmp_user.py ++++ b/lib/ansible/modules/network/nxos/nxos_snmp_user.py +@@ -245,7 +245,7 @@ def main(): + argument_spec = dict( + user=dict(required=True, type='str'), + group=dict(type='str'), +- pwd=dict(type='str'), ++ pwd=dict(type="str", no_log=True), + privacy=dict(type='str'), + authentication=dict(choices=['md5', 'sha']), + encrypt=dict(type='bool'), +diff --git a/lib/ansible/modules/network/nxos/nxos_vrrp.py b/lib/ansible/modules/network/nxos/nxos_vrrp.py +index b53386e..96593f7 100644 +--- a/lib/ansible/modules/network/nxos/nxos_vrrp.py ++++ b/lib/ansible/modules/network/nxos/nxos_vrrp.py +@@ -319,7 +319,7 @@ def main(): + admin_state=dict(required=False, type='str', + choices=['shutdown', 'no shutdown'], + default='no shutdown'), +- authentication=dict(required=False, type='str'), ++ authentication=dict(required=False, type="str", no_log=True), + state=dict(choices=['absent', 'present'], required=False, default='present') + ) + argument_spec.update(nxos_argument_spec) +-- +2.27.0 + diff --git a/ansible.spec b/ansible.spec index 65aaa38..f4c4cd2 100644 --- a/ansible.spec +++ b/ansible.spec @@ -3,12 +3,23 @@ Name: ansible Summary: SSH-based configuration management, deployment, and task execution system Version: 2.5.5 -Release: 1 +Release: 2 License: Python-2.0 and MIT and GPL+ Url: http://ansible.com Source0: https://releases.ansible.com/ansible/%{name}-%{version}.tar.gz Patch0: 0001-Changes-to-support-building-docs-with-old-jinja2.patch Patch100: ansible-newer-jinja.patch +Patch101: CVE-2019-14904.patch +Patch102: CVE-2020-10684.patch +Patch103: CVE-2020-10729.patch +Patch104: CVE-2020-1735.patch +Patch105: CVE-2020-1736.patch +Patch106: CVE-2020-1737.patch +Patch107: CVE-2020-1738.patch +Patch108: CVE-2020-1739.patch +Patch109: CVE-2020-1740.patch +Patch110: CVE-2020-1753.patch +Patch111: CVE-2021-20191.patch BuildArch: noarch Provides: ansible-fireball = %{version}-%{release} Obsoletes: ansible-fireball < 1.2.4 @@ -63,6 +74,18 @@ This package installs extensive documentation for ansible %setup -q %patch0 -p1 %patch100 -p1 +%patch101 -p1 +%patch102 -p1 +%patch103 -p1 +%patch104 -p1 +%patch105 -p1 +%patch106 -p1 +%patch107 -p1 +%patch108 -p1 +%patch109 -p1 +%patch110 -p1 +%patch111 -p1 + %if 0%{?with_python3} rm -rf %{py3dir} cp -a . %{py3dir} @@ -123,5 +146,8 @@ cp -pr docs/docsite/rst . %endif %changelog +* Fri Sep 17 2021 yaoxin - 2.5.5-2 +- Fix CVE-2019-14904 CVE-2020-10684 CVE-2020-10729 CVE-2020-1735-to-CVE-2020-1740 CVE-2020-1753 CVE-2021-20191 + * Tue Jan 12 2021 yanan li - 2.5.5-1 - Package init -- Gitee