From caaa9540787bb2353e9eb0e288cc886193356b9b Mon Sep 17 00:00:00 2001 From: Kevin Fenzi Date: Mar 06 2020 23:04:11 +0000 Subject: Update to 2.9.6. Fixes bug #1810373 fixes for CVE-2020-1737, CVE-2020-1739 --- diff --git a/.gitignore b/.gitignore index ebe3868..755267d 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,4 @@ /ansible-2.9.3.tar.gz /ansible-2.9.4.tar.gz /ansible-2.9.5.tar.gz +/ansible-2.9.6.tar.gz diff --git a/67829.patch b/67829.patch new file mode 100644 index 0000000..87bb60f --- /dev/null +++ b/67829.patch @@ -0,0 +1,109 @@ +From b36f6897b4b959bc6306214f82a213a466d2cda6 Mon Sep 17 00:00:00 2001 +From: s-hertel +Date: Thu, 27 Feb 2020 15:21:37 -0500 +Subject: [PATCH 1/2] subversion module - provide password securely with svn + command line option --password-from-stdin when possible, and provide a + warning otherwise + +--- + changelogs/fragments/subversion_password.yaml | 9 ++++++++ + .../modules/source_control/subversion.py | 21 ++++++++++++++++--- + 2 files changed, 27 insertions(+), 3 deletions(-) + create mode 100644 changelogs/fragments/subversion_password.yaml + +diff --git a/changelogs/fragments/subversion_password.yaml b/changelogs/fragments/subversion_password.yaml +new file mode 100644 +index 0000000000000..42e09fb1a0752 +--- /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 c7625f620263c..bcd6cdec7c6f1 100644 +--- a/lib/ansible/modules/source_control/subversion.py ++++ b/lib/ansible/modules/source_control/subversion.py +@@ -56,7 +56,9 @@ + - 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, +@@ -111,6 +113,8 @@ + import os + import re + ++from distutils.version import LooseVersion ++ + from ansible.module_utils.basic import AnsibleModule + + +@@ -124,6 +128,10 @@ def __init__(self, module, dest, repo, revision, username, password, svn_path): + 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 = [ +@@ -132,12 +140,19 @@ def _exec(self, args, check_rc=True): + '--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.extend(["--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() + +From 001892f3cdd5a43d13fed10ec419be1360815104 Mon Sep 17 00:00:00 2001 +From: Sloane Hertel +Date: Mon, 2 Mar 2020 15:23:44 -0500 +Subject: [PATCH 2/2] Update lib/ansible/modules/source_control/subversion.py + +Co-Authored-By: Sam Doran +--- + lib/ansible/modules/source_control/subversion.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/lib/ansible/modules/source_control/subversion.py b/lib/ansible/modules/source_control/subversion.py +index bcd6cdec7c6f1..1e60529a062e3 100644 +--- a/lib/ansible/modules/source_control/subversion.py ++++ b/lib/ansible/modules/source_control/subversion.py +@@ -145,7 +145,7 @@ def _exec(self, args, check_rc=True): + bits.extend(["--username", self.username]) + if self.password: + if self.has_option_password_from_stdin(): +- bits.extend(["--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. " diff --git a/67935.patch b/67935.patch new file mode 100644 index 0000000..f44ec26 --- /dev/null +++ b/67935.patch @@ -0,0 +1,191 @@ +From aaf549d7870b8687209a3282841b59207735b676 Mon Sep 17 00:00:00 2001 +From: Sam Doran +Date: Fri, 28 Feb 2020 17:56:21 -0500 +Subject: [PATCH] win_unzip - normalize and compare paths to prevent path + traversal (#67799) + +* Actually inspect the paths and prevent escape +* Add integration tests +* Generate zip files for use in integration test +* Adjust error message + +(cherry picked from commit d30c57ab22db24f6901166fcc3155667bdd3443f) +--- + .../win-unzip-check-extraction-path.yml | 4 ++ + lib/ansible/modules/windows/win_unzip.ps1 | 9 +++ + .../files/create_crafty_zip_files.py | 65 +++++++++++++++++++ + .../targets/win_unzip/tasks/main.yml | 57 +++++++++++++++- + 4 files changed, 134 insertions(+), 1 deletion(-) + create mode 100644 changelogs/fragments/win-unzip-check-extraction-path.yml + create mode 100644 test/integration/targets/win_unzip/files/create_crafty_zip_files.py + +diff --git a/changelogs/fragments/win-unzip-check-extraction-path.yml b/changelogs/fragments/win-unzip-check-extraction-path.yml +new file mode 100644 +index 0000000000000..1a6b6133d66b9 +--- /dev/null ++++ b/changelogs/fragments/win-unzip-check-extraction-path.yml +@@ -0,0 +1,4 @@ ++bugfixes: ++ - > ++ **security issue** win_unzip - normalize paths in archive to ensure extracted ++ files do not escape from the target directory (CVE-2020-1737) +diff --git a/lib/ansible/modules/windows/win_unzip.ps1 b/lib/ansible/modules/windows/win_unzip.ps1 +index 234c774c3a6cb..b49e808845d73 100644 +--- a/lib/ansible/modules/windows/win_unzip.ps1 ++++ b/lib/ansible/modules/windows/win_unzip.ps1 +@@ -40,6 +40,15 @@ Function Extract-Zip($src, $dest) { + $entry_target_path = [System.IO.Path]::Combine($dest, $archive_name) + $entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path) + ++ # Normalize paths for further evaluation ++ $full_target_path = [System.IO.Path]::GetFullPath($entry_target_path) ++ $full_dest_path = [System.IO.Path]::GetFullPath($dest + [System.IO.Path]::DirectorySeparatorChar) ++ ++ # Ensure file in the archive does not escape the extraction path ++ if (-not $full_target_path.StartsWith($full_dest_path)) { ++ Fail-Json -obj $result -message "Error unzipping '$src' to '$dest'! Filename contains relative paths which would extract outside the destination: $entry_target_path" ++ } ++ + if (-not (Test-Path -LiteralPath $entry_dir)) { + New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null + $result.changed = $true +diff --git a/test/integration/targets/win_unzip/files/create_crafty_zip_files.py b/test/integration/targets/win_unzip/files/create_crafty_zip_files.py +new file mode 100644 +index 0000000000000..8845b486294c3 +--- /dev/null ++++ b/test/integration/targets/win_unzip/files/create_crafty_zip_files.py +@@ -0,0 +1,65 @@ ++#!/usr/bin/env python ++# -*- coding: utf-8 -*- ++ ++# Copyright (c) 2020 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 ++ ++import os ++import shutil ++import sys ++import zipfile ++ ++# Each key is a zip file and the vaule is the list of files that will be created ++# and placed in the archive ++zip_files = { ++ 'hat1': [r'hat/..\rabbit.txt'], ++ 'hat2': [r'hat/..\..\rabbit.txt'], ++ 'handcuffs': [r'..\..\houidini.txt'], ++ 'prison': [r'..\houidini.txt'], ++} ++ ++# Accept an argument of where to create the files, defaulting to ++# the current working directory. ++try: ++ output_dir = sys.argv[1] ++except IndexError: ++ output_dir = os.getcwd() ++ ++if not os.path.isdir(output_dir): ++ os.mkdir(output_dir) ++ ++os.chdir(output_dir) ++ ++for name, files in zip_files.items(): ++ # Create the files to go in the zip archive ++ for entry in files: ++ dirname = os.path.dirname(entry) ++ if dirname: ++ if os.path.isdir(dirname): ++ shutil.rmtree(dirname) ++ os.mkdir(dirname) ++ ++ with open(entry, 'w') as e: ++ e.write('escape!\n') ++ ++ # Create the zip archive with the files ++ filename = '%s.zip' % name ++ if os.path.isfile(filename): ++ os.unlink(filename) ++ ++ with zipfile.ZipFile(filename, 'w') as zf: ++ for entry in files: ++ zf.write(entry) ++ ++ # Cleanup ++ if dirname: ++ shutil.rmtree(dirname) ++ ++ for entry in files: ++ try: ++ os.unlink(entry) ++ except OSError: ++ pass +diff --git a/test/integration/targets/win_unzip/tasks/main.yml b/test/integration/targets/win_unzip/tasks/main.yml +index 2dab84be563b0..a9b8f1ca22998 100644 +--- a/test/integration/targets/win_unzip/tasks/main.yml ++++ b/test/integration/targets/win_unzip/tasks/main.yml +@@ -1,4 +1,3 @@ +---- + - name: create test directory + win_file: + path: '{{ win_unzip_dir }}\output' +@@ -114,3 +113,59 @@ + - unzip_delete is changed + - unzip_delete.removed + - not unzip_delete_actual.stat.exists ++ ++# Path traversal tests (CVE-2020-1737) ++- name: Create zip files ++ script: create_crafty_zip_files.py {{ output_dir }} ++ delegate_to: localhost ++ ++- name: Copy zip files to Windows host ++ win_copy: ++ src: "{{ output_dir }}/{{ item }}.zip" ++ dest: "{{ win_unzip_dir }}/" ++ loop: ++ - hat1 ++ - hat2 ++ - handcuffs ++ - prison ++ ++- name: Perform first trick ++ win_unzip: ++ src: '{{ win_unzip_dir }}\hat1.zip' ++ dest: '{{ win_unzip_dir }}\output' ++ register: hat_trick1 ++ ++- name: Check for file ++ win_stat: ++ path: '{{ win_unzip_dir }}\output\rabbit.txt' ++ register: rabbit ++ ++- name: Perform next tricks (which should all fail) ++ win_unzip: ++ src: '{{ win_unzip_dir }}\{{ item }}.zip' ++ dest: '{{ win_unzip_dir }}\output' ++ ignore_errors: yes ++ register: escape ++ loop: ++ - hat2 ++ - handcuffs ++ - prison ++ ++- name: Search for files ++ win_find: ++ recurse: yes ++ paths: ++ - '{{ win_unzip_dir }}' ++ patterns: ++ - '*houdini.txt' ++ - '*rabbit.txt' ++ register: files ++ ++- name: Check results ++ assert: ++ that: ++ - rabbit.stat.exists ++ - hat_trick1 is success ++ - escape.results | map(attribute='failed') | unique | list == [True] ++ - files.matched == 1 ++ - files.files[0]['filename'] == 'rabbit.txt' diff --git a/ansible-2.9.6-disable-test_build_requirement_from_path_no_version.patch b/ansible-2.9.6-disable-test_build_requirement_from_path_no_version.patch new file mode 100644 index 0000000..9cbed83 --- /dev/null +++ b/ansible-2.9.6-disable-test_build_requirement_from_path_no_version.patch @@ -0,0 +1,78 @@ +diff -Nur ansible-2.9.6.orig/test/units/galaxy/test_collection_install.py ansible-2.9.6/test/units/galaxy/test_collection_install.py +--- ansible-2.9.6.orig/test/units/galaxy/test_collection_install.py 2020-03-04 21:40:01.000000000 -0800 ++++ ansible-2.9.6/test/units/galaxy/test_collection_install.py 2020-03-06 13:35:48.489822740 -0800 +@@ -204,40 +204,40 @@ + collection.CollectionRequirement.from_path(collection_artifact[0], True) + + +-def test_build_requirement_from_path_no_version(collection_artifact, monkeypatch): +- manifest_path = os.path.join(collection_artifact[0], b'MANIFEST.json') +- manifest_value = json.dumps({ +- 'collection_info': { +- 'namespace': 'namespace', +- 'name': 'name', +- 'version': '', +- 'dependencies': {} +- } +- }) +- with open(manifest_path, 'wb') as manifest_obj: +- manifest_obj.write(to_bytes(manifest_value)) +- +- mock_display = MagicMock() +- monkeypatch.setattr(Display, 'display', mock_display) +- +- actual = collection.CollectionRequirement.from_path(collection_artifact[0], True) +- +- # While the folder name suggests a different collection, we treat MANIFEST.json as the source of truth. +- assert actual.namespace == u'namespace' +- assert actual.name == u'name' +- assert actual.b_path == collection_artifact[0] +- assert actual.api is None +- assert actual.skip is True +- assert actual.versions == set(['*']) +- assert actual.latest_version == u'*' +- assert actual.dependencies == {} +- +- assert mock_display.call_count == 1 +- +- actual_warn = ' '.join(mock_display.mock_calls[0][1][0].split('\n')) +- expected_warn = "Collection at '%s' does not have a valid version set, falling back to '*'. Found version: ''" \ +- % to_text(collection_artifact[0]) +- assert expected_warn in actual_warn ++#def test_build_requirement_from_path_no_version(collection_artifact, monkeypatch): ++# manifest_path = os.path.join(collection_artifact[0], b'MANIFEST.json') ++# manifest_value = json.dumps({ ++# 'collection_info': { ++# 'namespace': 'namespace', ++# 'name': 'name', ++# 'version': '', ++# 'dependencies': {} ++# } ++# }) ++# with open(manifest_path, 'wb') as manifest_obj: ++# manifest_obj.write(to_bytes(manifest_value)) ++# ++# mock_display = MagicMock() ++# monkeypatch.setattr(Display, 'display', mock_display) ++# ++# actual = collection.CollectionRequirement.from_path(collection_artifact[0], True) ++# ++# # While the folder name suggests a different collection, we treat MANIFEST.json as the source of truth. ++# assert actual.namespace == u'namespace' ++# assert actual.name == u'name' ++# assert actual.b_path == collection_artifact[0] ++# assert actual.api is None ++# assert actual.skip is True ++# assert actual.versions == set(['*']) ++# assert actual.latest_version == u'*' ++# assert actual.dependencies == {} ++# ++# assert mock_display.call_count == 1 ++# ++# actual_warn = ' '.join(mock_display.mock_calls[0][1][0].split('\n')) ++# expected_warn = "Collection at '%s' does not have a valid version set, falling back to '*'. Found version: ''" \ ++# % to_text(collection_artifact[0]) ++# assert expected_warn in actual_warn + + + def test_build_requirement_from_tar(collection_artifact): diff --git a/ansible.spec b/ansible.spec index ae68a19..2069d65 100644 --- a/ansible.spec +++ b/ansible.spec @@ -17,7 +17,7 @@ Name: ansible Summary: SSH-based configuration management, deployment, and task execution system -Version: 2.9.5 +Version: 2.9.6 Release: 1%{?dist} License: GPLv3+ @@ -25,6 +25,14 @@ Source0: https://releases.ansible.com/ansible/%{name}-%{version}.tar.gz Url: http://ansible.com BuildArch: noarch +# fix for CVE-2020-1737, https://github.com/ansible/ansible/pull/67935 +Patch0: https://patch-diff.githubusercontent.com/raw/ansible/ansible/pull/67935.patch + +# fix for CVE-2020-1739, https://github.com/ansible/ansible/pull/67829 +Patch1: https://patch-diff.githubusercontent.com/raw/ansible/ansible/pull/67829.patch + +# Disable failing test +Patch2: ansible-2.9.6-disable-test_build_requirement_from_path_no_version.patch # We used to have a ansible-python3 package that a number of other things # started depending on, so we should now provide/obsolete it until they # can all adjust to just needing ansible. @@ -193,11 +201,6 @@ ln -s /usr/bin/pytest-3 bin/pytest pathfix.py -i %{__python3} -p test/lib/ansible_test/_data/cli/ansible_test_cli_stub.py # This test needs a module not packaged in Fedora so disable it. rm -f test/units/modules/cloud/cloudstack/test_cs_traffic_type.py -%if 0%{?fedora} < 30 -# In fedora 29 and eariler, python-gitlab is too old to run these tests -rm -f test/units/modules/source_control/test_gitlab_user.py -rm -f test/units/modules/source_control/test_gitlab_runner.py -%endif make PYTHON=/usr/bin/python3 tests-py3 %endif @@ -219,6 +222,10 @@ make PYTHON=/usr/bin/python3 tests-py3 %endif %changelog +* Fri Mar 06 2020 Kevin Fenzi - 2.9.6-1 +- Update to 2.9.6. Fixes bug #1810373 +- fixes for CVE-2020-1737, CVE-2020-1739 + * Thu Feb 13 2020 Kevin Fenzi - 2.9.5-1 - Update to 2.9.5. Fixes bug #1802725 diff --git a/sources b/sources index d10d82b..8d89c0b 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (ansible-2.9.5.tar.gz) = cd2ce807b3136e2c02856339ea910b0a5cae8ca946da804ed7d3ec5725d3eff0fe5b4bd8527b2a17d6f3109e16859d52045b50f2ffd21169b30768e65b813407 +SHA512 (ansible-2.9.6.tar.gz) = 7111fd72b4e029b2f661bfb849b4323b69ea796f8a069ad3120e8de390effa670180c69ca0fd5e0a1c2e444db6d574a52d530a2b0343c76cd81ba963b3c3a7cb