Attempt at reverting CVE-2020-1736 changes [2.10] (#71514)

* Revert atomic_move changes
* add note about mode reverts in porting guide

Signed-off-by: Rick Elrod <rick@elrod.me>
This commit is contained in:
Rick Elrod 2020-09-01 01:00:26 -05:00 committed by GitHub
parent 314834c6d3
commit 9a48ffd61b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 24 additions and 160 deletions

View file

@ -0,0 +1,2 @@
security_fixes:
- The fix for CVE-2020-1736 has been reverted. Users are encouraged to specify a ``mode`` parameter in their file-based tasks when the files being manipulated contain sensitive data.

View file

@ -47,80 +47,14 @@ Deprecated
Modules Modules
======= =======
Change to Default File Permissions
----------------------------------
To address `CVE-2020-1736 <https://nvd.nist.gov/vuln/detail/CVE-2020-1736>`_, the default permissions for certain files created by Ansible using ``atomic_move()`` were changed from ``0o666`` to ``0o600``. The default permissions value was only used for the temporary file before it was moved into its place or newly created files. If the file existed when the new temporary file was moved into place, Ansible would use the permissions of the existing file. If there was no existing file, Ansible would retain the default file permissions, combined with the system ``umask``, of the temporary file.
Most modules that call ``atomic_move()`` also call ``set_fs_attributes_if_different()`` or ``set_mode_if_different()``, which will set the permissions of the file to what is specified in the task.
A new warning will be displayed when all of the following conditions are true:
- The file at the final destination, not the temporary file, does not exist
- A module supports setting ``mode`` but it was not specified for the task
- The module calls ``atomic_move()`` but does not later call ``set_fs_attributes_if_different()`` or ``set_mode_if_different()`` with a ``mode`` specified
The following modules call ``atomic_move()`` but do not call ``set_fs_attributes_if_different()`` or ``set_mode_if_different()`` and do not support setting ``mode``. This means for files they create, the default permissions have changed and there is no indication:
- :ref:`ansible.builtin.known_hosts <ansible_collections.ansible.builtin.known_hosts_module>`
- :ref:`ansible.builtin.service <ansible_collections.ansible.builtin.service_module>`
Code Audit
~~~~~~~~~~
The code was audited for modules that use ``atomic_move()`` but **do not** later call ``set_fs_attributes_if_different()`` or ``set_mode_if_different()``. Modules that provide no means for specifying the ``mode`` will not display a warning message since there is no way for the playbook author to remove the warning. The behavior of each module with regards to the default permissions of temporary files and the permissions of newly created files is explained below.
known_hosts
^^^^^^^^^^^
The :ref:`ansible.builtin.known_hosts <ansible_collections.ansible.builtin.known_hosts_module>` module uses ``atomic_move()`` to operate on the ``known_hosts`` file specified by the ``path`` parameter in the module. It creates a temporary file using ``tempfile.NamedTemporaryFile()`` which creates a temporary file that is readable and writable only by the creating user ID.
service
^^^^^^^
The :ref:`ansible.builtin.service <ansible_collections.ansible.builtin.service_module>` module uses ``atomic_move()`` to operate on the default rc file, which is the first found of ``/etc/rc.conf``, ``/etc/rc.conf.local``, and ``/usr/local/etc/rc.conf``. Since these files almost always exist on the target system, they will not be created and the existing permissions of the file will be used.
**The following modules were included in Ansible <= 2.9. They have moved to collections but are documented here for completeness.**
authorized_key
^^^^^^^^^^^^^^
The :ref:`ansible.posix.authorized_key <ansible_collections.ansible.posix.authorized_key_module>` module uses ``atomic_move()`` to operate on the the ``authorized_key`` file. A temporary file is created with ``tempfile.mkstemp()`` before being moved into place. The temporary file is readable and writable only by the creating user ID. The :ref:`ansible.builtin.authorized_key <ansible_collections.ansible.posix.authorized_key_module>` module manages the permissions of the the ``.ssh`` direcotry and ``authorized_keys`` files if ``managed_dirs`` is set to ``True``, which is the default. The module sets the ``ssh`` directory owner and group to the ``uid`` and ``gid`` of the user specified in the ``user`` parameter and directory permissions to ``700``. The module sets the ``authorized_key`` file owner and group to the ``uid`` and ``gid`` of the user specified in the ``user`` parameter and file permissions to ``600``. These values cannot be controlled by module parameters.
interfaces_file
^^^^^^^^^^^^^^^
The :ref:`community.general.interfaces_file <ansible_collections.community.general.interfaces_file_module>` module uses ``atomic_move()`` to operate on ``/etc/network/serivces`` or the ``dest`` specified by the module. A temporary file is created with ``tempfile.mkstemp()`` before being moved into place. The temporary file is readable and writable only by the creating user ID. If the file specified by ``path`` does not exist it will retain the permissions of the temporary file once moved into place.
pam_limits
^^^^^^^^^^
The :ref:`community.general.pam_limits <ansible_collections.community.general.pam_limits_module>` module uses ``atomic_move()`` to operate on ``/etc/security/limits.conf`` or the value of ``dest``. A temporary file is created using ``tempfile.NamedTemporaryFile()``, which is only readable and writable by the creating user ID. The temporary file will inherit the permissions of the file specified by ``dest``, or it will retain the permissions that only allow the creating user ID to read and write the file.
pamd
^^^^
The :ref:`community.general.pamd <ansible_collections.community.general.pamd_module>` module uses ``atomic_move()`` to operate on a file in ``/etc/pam.d``. The path and the file can be specified by setting the ``path`` and ``name`` parameters. A temporary file is created using ``tempfile.NamedTemporaryFile()``, which is only readable and writable by the creating user ID. The temporary file will inherit the permissions of the file located at ``[dest]/[name]``, or it will retain the permissions of the temporary file that only allow the creating user ID to read and write the file.
redhat_subscription
^^^^^^^^^^^^^^^^^^^
The :ref:`community.general.redhat_subscription <ansible_collections.community.general.redhat_subscription_module>` module uses ``atomic_move()`` to operate on ``/etc/yum/pluginconf.d/rhnplugin.conf`` and ``/etc/yum/pluginconf.d/subscription-manager.conf``. A temporary file is created with ``tempfile.mkstemp()`` before being moved into place. The temporary file is readable and writable only by the creating user ID and the temporary file will inherit the permissions of the existing file once it is moved in to place.
selinux
^^^^^^^
The :ref:`ansible.posix.selinux <ansible_collections.ansible.posix.selinux_module>` module uses ``atomic_move()`` to operate on ``/etc/selinux/config`` on the value specified by ``configfile``. The module will fail if ``configfile`` does not exist before any temporary data is written to disk. A temporary file is created with ``tempfile.mkstemp()`` before being moved into place. The temporary file is readable and writable only by the creating user ID. Since the file specified by ``configfile`` must exist, the temporary file will inherit the permissions of that file once it is moved in to place.
sysctl
^^^^^^
The :ref:`ansible.posix.sysctl <ansible_collections.ansible.posix.sysctl_module>` module uses ``atomic_move()`` to operate on ``/etc/sysctl.conf`` or the value specified by ``sysctl_file``. The module will fail if ``sysctl_file`` does not exist before any temporary data is written to disk. A temporary file is created with ``tempfile.mkstemp()`` before being moved into place. The temporary file is readable and writable only by the creating user ID. Since the file specified by ``sysctl_file`` must exist, the temporary file will inherit the permissions of that file once it is moved in to place.
.. warning:: .. warning::
Links on this page may not point to the most recent versions of modules. We will update them when we can. Links on this page may not point to the most recent versions of modules. We will update them when we can.
* Version 2.10.0 of ansible-base changed the default mode of file-based tasks to ``0o600`` when the user did not specify a ``mode`` parameter on file-based tasks. This was in response to a CVE report which we have reconsidered and no longer consider a flaw in ansible-base. As a result, the ``mode`` change has been reverted in 2.10.1, and ``mode`` will now default to ``0o666`` as in previous versions of Ansible and previous RCs of ansible-base.
* If you changed any tasks to specify less restrictive permissions while using 2.10.0, those changes will be unnecessary (but will do no harm) in 2.10.1.
* To avoid the issue raised in CVE-2020-1736, specify a ``mode`` parameter in all file-based tasks that accept it.
Noteworthy module changes Noteworthy module changes
------------------------- -------------------------

View file

@ -1125,9 +1125,6 @@ class AnsibleModule(object):
def set_mode_if_different(self, path, mode, changed, diff=None, expand=True): def set_mode_if_different(self, path, mode, changed, diff=None, expand=True):
if mode is None:
return changed
# Remove paths so we do not warn about creating with default permissions # Remove paths so we do not warn about creating with default permissions
# since we are calling this method on the path and setting the specified mode. # since we are calling this method on the path and setting the specified mode.
try: try:
@ -1135,6 +1132,9 @@ class AnsibleModule(object):
except KeyError: except KeyError:
pass pass
if mode is None:
return changed
b_path = to_bytes(path, errors='surrogate_or_strict') b_path = to_bytes(path, errors='surrogate_or_strict')
if expand: if expand:
b_path = os.path.expanduser(os.path.expandvars(b_path)) b_path = os.path.expanduser(os.path.expandvars(b_path))

View file

@ -59,7 +59,7 @@ PERMS_RE = re.compile(r'[^rwxXstugo]')
_PERM_BITS = 0o7777 # file mode permission bits _PERM_BITS = 0o7777 # file mode permission bits
_EXEC_PERM_BITS = 0o0111 # execute permission bits _EXEC_PERM_BITS = 0o0111 # execute permission bits
_DEFAULT_PERM = 0o0600 # default file permission bits _DEFAULT_PERM = 0o0666 # default file permission bits
def is_executable(path): def is_executable(path):

View file

@ -8,7 +8,7 @@
test_repo_spec: "deb http://apt.postgresql.org/pub/repos/apt/ {{ ansible_distribution_release }}-pgdg main" test_repo_spec: "deb http://apt.postgresql.org/pub/repos/apt/ {{ ansible_distribution_release }}-pgdg main"
test_repo_path: /etc/apt/sources.list.d/apt_postgresql_org_pub_repos_apt.list test_repo_path: /etc/apt/sources.list.d/apt_postgresql_org_pub_repos_apt.list
- import_tasks: mode_cleanup.yaml - include: mode_cleanup.yaml
- name: Add GPG key to verify signatures - name: Add GPG key to verify signatures
apt_key: apt_key:
@ -31,7 +31,7 @@
debug: debug:
var: mode_given_yaml_literal_0600 var: mode_given_yaml_literal_0600
- import_tasks: mode_cleanup.yaml - include: mode_cleanup.yaml
- name: Assert mode_given_yaml_literal_0600 is correct - name: Assert mode_given_yaml_literal_0600 is correct
assert: assert:
@ -52,11 +52,11 @@
debug: debug:
var: no_mode_stat var: no_mode_stat
- import_tasks: mode_cleanup.yaml - include: mode_cleanup.yaml
- name: Assert no_mode_stat is correct - name: Assert no_mode_stat is correct
assert: assert:
that: "no_mode_stat.stat.mode == '0600'" that: "no_mode_stat.stat.mode == '0644'"
- name: Mode specified as string 0600 - name: Mode specified as string 0600
apt_repository: apt_repository:
@ -74,7 +74,7 @@
debug: debug:
var: mode_given_string_stat var: mode_given_string_stat
- import_tasks: mode_cleanup.yaml - include: mode_cleanup.yaml
- name: Mode specified as string 600 - name: Mode specified as string 600
apt_repository: apt_repository:
@ -92,7 +92,7 @@
debug: debug:
var: mode_given_string_600_stat var: mode_given_string_600_stat
- import_tasks: mode_cleanup.yaml - include: mode_cleanup.yaml
- name: Assert mode is correct - name: Assert mode is correct
assert: assert:
@ -114,7 +114,7 @@
debug: debug:
var: mode_given_yaml_literal_600 var: mode_given_yaml_literal_600
- import_tasks: mode_cleanup.yaml - include: mode_cleanup.yaml
# a literal 600 as the mode will fail currently, in the sense that it # a literal 600 as the mode will fail currently, in the sense that it
# doesn't guess and consider 600 and 0600 to be the same, and will instead # doesn't guess and consider 600 and 0600 to be the same, and will instead
@ -127,4 +127,4 @@
# See https://github.com/ansible/ansible/issues/16370 # See https://github.com/ansible/ansible/issues/16370
- name: Assert mode_given_yaml_literal_600 is correct - name: Assert mode_given_yaml_literal_600 is correct
assert: assert:
that: "mode_given_yaml_literal_600.stat.mode == '1130'" that: "mode_given_yaml_literal_600.stat.mode == '1130'"

View file

@ -1 +0,0 @@
shippable/posix/group5

View file

@ -1,36 +0,0 @@
#!/usr/bin/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 tempfile
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(
argument_spec={
'dest': {'type': 'path'},
'call_fs_attributes': {'type': 'bool', 'default': True},
},
add_file_common_args=True,
)
results = {}
with tempfile.NamedTemporaryFile(delete=False) as tf:
file_args = module.load_file_common_arguments(module.params)
module.atomic_move(tf.name, module.params['dest'])
if module.params['call_fs_attributes']:
results['changed'] = module.set_fs_attributes_if_different(file_args, True)
module.exit_json(**results)
if __name__ == '__main__':
main()

View file

@ -1,2 +0,0 @@
dependencies:
- setup_remote_tmp_dir

View file

@ -1,33 +0,0 @@
- name: Run task with no mode
test_perm_warning:
dest: "{{ remote_tmp_dir }}/endangerdisown"
register: no_mode_results
- name: Run task with mode
test_perm_warning:
mode: '0644'
dest: "{{ remote_tmp_dir }}/groveestablish"
register: with_mode_results
- name: Run task without calling set_fs_attributes_if_different()
test_perm_warning:
call_fs_attributes: no
dest: "{{ remote_tmp_dir }}/referabletank"
register: skip_fs_attributes
- stat:
path: "{{ remote_tmp_dir }}/{{ item }}"
loop:
- endangerdisown
- groveestablish
register: files
- name: Ensure we get a warning when appropriate
assert:
that:
- no_mode_results.warnings | default([], True) | length == 1
- "'created with default permissions' in no_mode_results.warnings[0]"
- files.results[0]['stat']['mode'] == '0600'
- files.results[1]['stat']['mode'] == '0644'
- with_mode_results.warnings is not defined # The Jinja version on CentOS 6 does not support default([], True)
- skip_fs_attributes.warnings | default([], True) | length == 1

View file

@ -63,7 +63,7 @@ def atomic_mocks(mocker, monkeypatch):
@pytest.fixture @pytest.fixture
def fake_stat(mocker): def fake_stat(mocker):
stat1 = mocker.MagicMock() stat1 = mocker.MagicMock()
stat1.st_mode = 0o0600 stat1.st_mode = 0o0644
stat1.st_uid = 0 stat1.st_uid = 0
stat1.st_gid = 0 stat1.st_gid = 0
stat1.st_flags = 0 stat1.st_flags = 0
@ -80,7 +80,7 @@ def test_new_file(atomic_am, atomic_mocks, mocker, selinux):
atomic_am.atomic_move('/path/to/src', '/path/to/dest') atomic_am.atomic_move('/path/to/src', '/path/to/dest')
atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/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 & ~0o022)] assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/dest', basic.DEFAULT_PERM & ~18)]
if selinux: if selinux:
assert atomic_am.selinux_default_context.call_args_list == [mocker.call('/path/to/dest')] assert atomic_am.selinux_default_context.call_args_list == [mocker.call('/path/to/dest')]
@ -101,7 +101,7 @@ def test_existing_file(atomic_am, atomic_mocks, fake_stat, mocker, selinux):
atomic_am.atomic_move('/path/to/src', '/path/to/dest') atomic_am.atomic_move('/path/to/src', '/path/to/dest')
atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/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 & ~0o022)] assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/src', basic.DEFAULT_PERM & ~18)]
if selinux: if selinux:
assert atomic_am.set_context_if_different.call_args_list == [mocker.call('/path/to/dest', mock_context, False)] assert atomic_am.set_context_if_different.call_args_list == [mocker.call('/path/to/dest', mock_context, False)]
@ -124,7 +124,7 @@ def test_no_tty_fallback(atomic_am, atomic_mocks, fake_stat, mocker):
atomic_am.atomic_move('/path/to/src', '/path/to/dest') atomic_am.atomic_move('/path/to/src', '/path/to/dest')
atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/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 & ~0o022)] 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.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')] assert atomic_am.selinux_context.call_args_list == [mocker.call('/path/to/dest')]
@ -154,7 +154,7 @@ def test_existing_file_stat_perms_failure(atomic_am, atomic_mocks, mocker):
atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/path/to/dest') atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/path/to/dest')
# FIXME: Should atomic_move() set a default permission value when it cannot retrieve the # 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. # 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 & ~0o022)] # 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.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')] assert atomic_am.selinux_context.call_args_list == [mocker.call('/path/to/dest')]
@ -211,7 +211,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') 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'), 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')] 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 & ~0o022)] assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/dest', basic.DEFAULT_PERM & ~18)]
if selinux: if selinux:
assert atomic_am.selinux_default_context.call_args_list == [mocker.call('/path/to/dest')] assert atomic_am.selinux_default_context.call_args_list == [mocker.call('/path/to/dest')]