!41 Fix CVE-2019-3828 and hostname module support openEuler and add defination of is_subpath and remove module_response_deepcopy

From: @wk333 
Reviewed-by: @myeuler 
Signed-off-by: @myeuler
This commit is contained in:
openeuler-ci-bot 2022-02-28 11:23:24 +00:00 committed by Gitee
commit fe7ba89ed2
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
5 changed files with 328 additions and 51 deletions

150
CVE-2019-3828.patch Normal file
View File

@ -0,0 +1,150 @@
From f3edc091523fbe301926b7a0db25fbbd96940d93 Mon Sep 17 00:00:00 2001
From: Matt Martz <matt@sivel.net>
Date: Mon, 18 Feb 2019 10:45:05 -0600
Subject: [PATCH] [stable-2.5] Disallow use of remote home directories
containing .. in their path (CVE-2019-3828) (#52133) (#52175)
* Disallow use of remote home directories containing .. in their path
* Add CVE to changelog
(cherry picked from commit b34d141)
Co-authored-by: Matt Martz <matt@sivel.net>
---
.../fragments/disallow-relative-homedir.yaml | 3 +
lib/ansible/plugins/action/__init__.py | 3 +
test/units/plugins/action/test_action.py | 61 ++++++++++++-------
3 files changed, 44 insertions(+), 23 deletions(-)
create mode 100644 changelogs/fragments/disallow-relative-homedir.yaml
diff --git a/changelogs/fragments/disallow-relative-homedir.yaml b/changelogs/fragments/disallow-relative-homedir.yaml
new file mode 100644
index 00000000000000..0ae36ef94d27fd
--- /dev/null
+++ b/changelogs/fragments/disallow-relative-homedir.yaml
@@ -0,0 +1,3 @@
+bugfixes:
+- remote home directory - Disallow use of remote home directories that include
+ relative pathing by means of `..` (CVE-2019-3828) (https://github.com/ansible/ansible/pull/52133)
diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py
index ba8f32e52983f8..23d95ee444bc23 100644
--- a/lib/ansible/plugins/action/__init__.py
+++ b/lib/ansible/plugins/action/__init__.py
@@ -609,6 +609,9 @@ def _remote_expand_user(self, path, sudoable=True, pathsep=None):
else:
expanded = initial_fragment
+ if '..' in os.path.dirname(expanded).split('/'):
+ raise AnsibleError("'%s' returned an invalid relative home directory path containing '..'" % self._play_context.remote_addr)
+
return expanded
def _strip_success_message(self, data):
diff --git a/test/units/plugins/action/test_action.py b/test/units/plugins/action/test_action.py
index 52fa3bea1bece4..5221e54ed4fafb 100644
--- a/test/units/plugins/action/test_action.py
+++ b/test/units/plugins/action/test_action.py
@@ -56,6 +56,28 @@
"""
+def _action_base():
+ fake_loader = DictDataLoader({
+ })
+ mock_module_loader = MagicMock()
+ mock_shared_loader_obj = MagicMock()
+ mock_shared_loader_obj.module_loader = mock_module_loader
+ mock_connection_loader = MagicMock()
+
+ mock_shared_loader_obj.connection_loader = mock_connection_loader
+ mock_connection = MagicMock()
+
+ play_context = MagicMock()
+
+ action_base = DerivedActionBase(task=None,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=fake_loader,
+ templar=None,
+ shared_loader_obj=mock_shared_loader_obj)
+ return action_base
+
+
class DerivedActionBase(ActionBase):
TRANSFERS_FILES = False
@@ -522,6 +544,18 @@ def test_action_base_sudo_only_if_user_differs(self):
finally:
C.BECOME_ALLOW_SAME_USER = become_allow_same_user
+ def test__remote_expand_user_relative_pathing(self):
+ action_base = _action_base()
+ action_base._play_context.remote_addr = 'bar'
+ action_base._low_level_execute_command = MagicMock(return_value={'stdout': b'../home/user'})
+ action_base._connection._shell.join_path.return_value = '../home/user/foo'
+ with self.assertRaises(AnsibleError) as cm:
+ action_base._remote_expand_user('~/foo')
+ self.assertEqual(
+ cm.exception.message,
+ "'bar' returned an invalid relative home directory path containing '..'"
+ )
+
class TestActionBaseCleanReturnedData(unittest.TestCase):
def test(self):
@@ -573,27 +607,8 @@ def fake_all(path_only=None):
class TestActionBaseParseReturnedData(unittest.TestCase):
- def _action_base(self):
- fake_loader = DictDataLoader({
- })
- mock_module_loader = MagicMock()
- mock_shared_loader_obj = MagicMock()
- mock_shared_loader_obj.module_loader = mock_module_loader
- mock_connection_loader = MagicMock()
-
- mock_shared_loader_obj.connection_loader = mock_connection_loader
- mock_connection = MagicMock()
-
- action_base = DerivedActionBase(task=None,
- connection=mock_connection,
- play_context=None,
- loader=fake_loader,
- templar=None,
- shared_loader_obj=mock_shared_loader_obj)
- return action_base
-
def test_fail_no_json(self):
- action_base = self._action_base()
+ action_base = _action_base()
rc = 0
stdout = 'foo\nbar\n'
err = 'oopsy'
@@ -607,7 +622,7 @@ def test_fail_no_json(self):
self.assertEqual(res['module_stderr'], err)
def test_json_empty(self):
- action_base = self._action_base()
+ action_base = _action_base()
rc = 0
stdout = '{}\n'
err = ''
@@ -621,7 +636,7 @@ def test_json_empty(self):
self.assertFalse(res)
def test_json_facts(self):
- action_base = self._action_base()
+ action_base = _action_base()
rc = 0
stdout = '{"ansible_facts": {"foo": "bar", "ansible_blip": "blip_value"}}\n'
err = ''
@@ -637,7 +652,7 @@ def test_json_facts(self):
# self.assertIsInstance(res['ansible_facts'], AnsibleUnsafe)
def test_json_facts_add_host(self):
- action_base = self._action_base()
+ action_base = _action_base()
rc = 0
stdout = '''{"ansible_facts": {"foo": "bar", "ansible_blip": "blip_value"},
"add_host": {"host_vars": {"some_key": ["whatever the add_host object is"]}

View File

@ -51,10 +51,10 @@ index c83709d..0a4fe07 100644
- # 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])
+ deprefixed[k[8:]] = deepcopy(facts[k])
else:
- deprefixed[k.replace('ansible_', '', 1)] = deepcopy(facts[k])
+ deprefixed[k] = module_response_deepcopy(facts[k])
+ deprefixed[k] = 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

View File

@ -1,53 +1,100 @@
From 5292482553dc409081f7f4368398358cbf9f8672 Mon Sep 17 00:00:00 2001
From: Brian Coca <bcoca@users.noreply.github.com>
Date: Wed, 8 Apr 2020 14:28:51 -0400
From c1dedd38cb6a3ad215cf077c533c93bd978acb93 Mon Sep 17 00:00:00 2001
From: caodongxia <315816521@qq.com>
Date: Tue, 2 Nov 2021 17:26:43 +0800
Subject: [PATCH] fixed fetch traversal from slurp (#68720)
Reference:https://github.com/ansible/ansible/commit/290bfa820d533dc224e0c3fa7dd7c6b907ed0189.patch
* 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 +++++++++++++++++
changelogs/fragments/fetch_no_slurp.yml | 2 +
lib/ansible/plugins/action/fetch.py | 44 ++++++++-----------
lib/ansible/utils/path.py | 22 ++++++++++
test/integration/targets/fetch/aliases | 1 +
.../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(-)
.../targets/fetch/injection/library/slurp.py | 28 ++++++++++++
.../targets/fetch/run_fetch_tests.yml | 5 +++
test/integration/targets/fetch/runme.sh | 12 +++++
9 files changed, 113 insertions(+), 26 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
create mode 100644 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
index 0000000000000..c742d40c3ba82
--- /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
index fbaf992..7db5b43 100644
--- a/lib/ansible/plugins/action/fetch.py
+++ b/lib/ansible/plugins/action/fetch.py
@@ -106,12 +106,6 @@ class ActionModule(ActionBase):
@@ -20,13 +20,13 @@ __metaclass__ = type
import os
import base64
-from ansible.errors import AnsibleError
+from ansible.errors import AnsibleActionFail, AnsibleActionSkip
from ansible.module_utils._text import to_bytes
from ansible.module_utils.six import string_types
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.action import ActionBase
from ansible.utils.hashing import checksum, checksum_s, md5, secure_hash
-from ansible.utils.path import makedirs_safe
+from ansible.utils.path import makedirs_safe, is_subpath
try:
from __main__ import display
@@ -47,24 +47,23 @@ class ActionModule(ActionBase):
try:
if self._play_context.check_mode:
- result['skipped'] = True
- result['msg'] = 'check mode not (yet) supported for this module'
- return result
+ raise AnsibleActionSkip('check mode not (yet) supported for this module')
source = self._task.args.get('src', None)
- dest = self._task.args.get('dest', None)
+ original_dest = dest = self._task.args.get('dest', None)
flat = boolean(self._task.args.get('flat'), strict=False)
fail_on_missing = boolean(self._task.args.get('fail_on_missing', True), strict=False)
validate_checksum = boolean(self._task.args.get('validate_checksum',
self._task.args.get('validate_md5', True)),
strict=False)
+ msg = ''
# validate source and dest are strings FIXME: use basic.py and module specs
if not isinstance(source, string_types):
- result['msg'] = "Invalid type supplied for source option, it must be a string"
+ msg = "Invalid type supplied for source option, it must be a string"
if not isinstance(dest, string_types):
- result['msg'] = "Invalid type supplied for dest option, it must be a string"
+ msg = "Invalid type supplied for dest option, it must be a string"
# validate_md5 is the deprecated way to specify validate_checksum
if 'validate_md5' in self._task.args and 'validate_checksum' in self._task.args:
@@ -74,11 +73,10 @@ class ActionModule(ActionBase):
display.deprecated('Use validate_checksum instead of validate_md5', version='2.8')
if source is None or dest is None:
- result['msg'] = "src and dest are required"
+ msg = "src and dest are required"
- if result.get('msg'):
- result['failed'] = True
- return result
+ if msg:
+ raise AnsibleActionFail(msg)
source = self._connection._shell.join_path(source)
source = self._remote_expand_user(source)
@@ -106,12 +104,6 @@ class ActionModule(ActionBase):
remote_data = base64.b64decode(slurpres['content'])
if remote_data is not None:
remote_checksum = checksum_s(remote_data)
@ -60,7 +107,7 @@ index fbaf992..471732c 100644
# calculate the destination name
if os.path.sep not in self._connection._shell.join_path('a', ''):
@@ -120,13 +114,14 @@ class ActionModule(ActionBase):
@@ -120,13 +112,13 @@ class ActionModule(ActionBase):
else:
source_local = source
@ -68,8 +115,7 @@ index fbaf992..471732c 100644
+ # 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))
+
+ 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"
@ -80,7 +126,7 @@ index fbaf992..471732c 100644
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):
@@ -143,8 +135,6 @@ class ActionModule(ActionBase):
target_name = self._play_context.remote_addr
dest = "%s/%s/%s" % (self._loader.path_dwim(dest), target_name, source_local)
@ -89,16 +135,16 @@ index fbaf992..471732c 100644
if remote_checksum in ('0', '1', '2', '3', '4', '5'):
result['changed'] = False
result['file'] = source
@@ -172,6 +165,8 @@ class ActionModule(ActionBase):
@@ -172,6 +162,8 @@ class ActionModule(ActionBase):
result['msg'] += ", not transferring, ignored"
return result
+ dest = os.path.normpath(dest)
+ dest = os.path.normpath(dest)
+
# calculate checksum for the local file
local_checksum = checksum(dest)
@@ -188,7 +183,7 @@ class ActionModule(ActionBase):
@@ -188,7 +180,7 @@ class ActionModule(ActionBase):
f.write(remote_data)
f.close()
except (IOError, OSError) as e:
@ -107,9 +153,46 @@ index fbaf992..471732c 100644
new_checksum = secure_hash(dest)
# For backwards compatibility. We'll return None on FIPS enabled systems
try:
diff --git a/lib/ansible/utils/path.py b/lib/ansible/utils/path.py
index 717cef6..2ef1316 100644
--- a/lib/ansible/utils/path.py
+++ b/lib/ansible/utils/path.py
@@ -97,3 +97,25 @@ def basedir(source):
dname = os.path.abspath(dname)
return to_text(dname, errors='surrogate_or_strict')
+
+def is_subpath(child, parent):
+ """
+ Compares paths to check if one is contained in the other
+ :arg: child: Path to test
+ :arg parent; Path to test against
+ """
+ test = False
+
+ abs_child = unfrackpath(child, follow=False)
+ abs_parent = unfrackpath(parent, follow=False)
+
+ c = abs_child.split(os.path.sep)
+ p = abs_parent.split(os.path.sep)
+
+ try:
+ test = c[:len(p)] == p
+ except IndexError:
+ # child is shorter than parent so cannot be subpath
+ pass
+
+ return test
diff --git a/test/integration/targets/fetch/aliases b/test/integration/targets/fetch/aliases
index 7af8b7f..197794b 100644
--- a/test/integration/targets/fetch/aliases
+++ b/test/integration/targets/fetch/aliases
@@ -1 +1,2 @@
posix/ci/group2
+needs/target/setup_remote_tmp_dir
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
index 0000000..c44b85e
--- /dev/null
+++ b/test/integration/targets/fetch/injection/avoid_slurp_return.yml
@@ -0,0 +1,26 @@
@ -148,10 +231,10 @@ index 0000000..493021b
+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
index 0000000..3916ae8
--- /dev/null
+++ b/test/integration/targets/fetch/injection/library/slurp.py
@@ -0,0 +1,29 @@
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
@ -171,7 +254,6 @@ index 0000000..7b78ba1
+
+import json
+import random
+
+bad_responses = ['../foo', '../../foo', '../../../foo', '/../../../foo', '/../foo', '//..//foo', '..//..//foo']
+
+
@ -193,8 +275,8 @@ index 0000000..f2ff1df
+ 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
new file mode 100644
index 0000000..3d5b75c
--- /dev/null
+++ b/test/integration/targets/fetch/runme.sh
@@ -0,0 +1,12 @@

View File

@ -0,0 +1,38 @@
diff --color -Nur ansible-2.5.5.orig/lib/ansible/modules/system/hostname.py ansible-2.5.5/lib/ansible/modules/system/hostname.py
--- ansible-2.5.5.orig/lib/ansible/modules/system/hostname.py 2018-06-15 05:20:41.000000000 +0800
+++ ansible-2.5.5/lib/ansible/modules/system/hostname.py 2021-12-16 16:28:49.652405482 +0800
@@ -762,6 +762,10 @@
distribution = 'Neon'
strategy_class = DebianStrategy
+class OpenEulerHostname(Hostname):
+ platform = 'Linux'
+ distribution = 'Openeuler'
+ strategy_class = SystemdStrategy
def main():
module = AnsibleModule(
diff --color -Nur ansible-2.5.5.orig/lib/ansible/module_utils/facts/system/distribution.py ansible-2.5.5/lib/ansible/module_utils/facts/system/distribution.py
--- ansible-2.5.5.orig/lib/ansible/module_utils/facts/system/distribution.py 2018-06-15 05:20:41.000000000 +0800
+++ ansible-2.5.5/lib/ansible/module_utils/facts/system/distribution.py 2021-12-16 14:16:19.859943412 +0800
@@ -408,7 +408,7 @@
# keep keys in sync with Conditionals page of docs
OS_FAMILY_MAP = {'RedHat': ['RedHat', 'Fedora', 'CentOS', 'Scientific', 'SLC',
'Ascendos', 'CloudLinux', 'PSBM', 'OracleLinux', 'OVS',
- 'OEL', 'Amazon', 'Virtuozzo', 'XenServer'],
+ 'OEL', 'Amazon', 'Virtuozzo', 'XenServer', 'openEuler'],
'Debian': ['Debian', 'Ubuntu', 'Raspbian', 'Neon', 'KDE neon',
'Linux Mint'],
'Suse': ['SuSE', 'SLES', 'SLED', 'openSUSE', 'openSUSE Tumbleweed',
diff --color -Nur ansible-2.5.5.orig/test/sanity/import/lib/ansible/module_utils/facts/system/distribution.py ansible-2.5.5/test/sanity/import/lib/ansible/module_utils/facts/system/distribution.py
--- ansible-2.5.5.orig/test/sanity/import/lib/ansible/module_utils/facts/system/distribution.py 2018-06-15 05:20:41.000000000 +0800
+++ ansible-2.5.5/test/sanity/import/lib/ansible/module_utils/facts/system/distribution.py 2021-12-16 14:16:19.859943412 +0800
@@ -408,7 +408,7 @@
# keep keys in sync with Conditionals page of docs
OS_FAMILY_MAP = {'RedHat': ['RedHat', 'Fedora', 'CentOS', 'Scientific', 'SLC',
'Ascendos', 'CloudLinux', 'PSBM', 'OracleLinux', 'OVS',
- 'OEL', 'Amazon', 'Virtuozzo', 'XenServer'],
+ 'OEL', 'Amazon', 'Virtuozzo', 'XenServer', 'openEuler'],
'Debian': ['Debian', 'Ubuntu', 'Raspbian', 'Neon', 'KDE neon',
'Linux Mint'],
'Suse': ['SuSE', 'SLES', 'SLED', 'openSUSE', 'openSUSE Tumbleweed',

View File

@ -3,7 +3,7 @@
Name: ansible
Summary: SSH-based configuration management, deployment, and task execution system
Version: 2.5.5
Release: 4
Release: 5
License: Python-2.0 and MIT and GPL+
Url: http://ansible.com
Source0: https://releases.ansible.com/ansible/%{name}-%{version}.tar.gz
@ -12,7 +12,6 @@ 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
Patch106: CVE-2020-1737.patch
Patch108: CVE-2020-1739.patch
Patch109: CVE-2020-1740.patch
@ -20,6 +19,9 @@ Patch110: CVE-2020-1753.patch
Patch111: CVE-2021-20191.patch
Patch112: CVE-2019-10156-1.patch
Patch113: CVE-2019-10156-2.patch
Patch114: CVE-2020-1735.patch
Patch115: CVE-2019-3828.patch
Patch116: ansible-2.5.5-openEuler-hostname.patch
BuildArch: noarch
Provides: ansible-fireball = %{version}-%{release}
Obsoletes: ansible-fireball < 1.2.4
@ -77,7 +79,6 @@ This package installs extensive documentation for ansible
%patch101 -p1
%patch102 -p1
%patch103 -p1
%patch104 -p1
%patch106 -p1
%patch108 -p1
%patch109 -p1
@ -85,7 +86,9 @@ This package installs extensive documentation for ansible
%patch111 -p1
%patch112 -p1
%patch113 -p1
%patch114 -p1
%patch115 -p1
%patch116 -p1
%if 0%{?with_python3}
rm -rf %{py3dir}
cp -a . %{py3dir}
@ -146,6 +149,10 @@ cp -pr docs/docsite/rst .
%endif
%changelog
* Mon Feb 28 2022 wangkai <wangkai385@huawei.com> - 2.5.5-5
- Fix CVE-2019-3828 and hostname module support openEuler
- and add defination of is_subpath and remove module_response_deepcopy
* Thu Oct 28 2021 liwu <liwu13@huawei.com> - 2.5.5-4
- The upstream community rolls back the patch