Prevent execution of locally overridden core modules

We greylist some modules in our action plugin blocking allowing them to
execute local code as long as it falls within safe constraints. Due to
the way ansible module loading works, a user could attack this by
creating a module in a local role or adjacent to a playbook that has the
same name as one of the modules we allow limited local execution. If
they did that it would allow them to execute arbitrary python code on
the executor.

Find the path of the module that will be executed in these cases and if
it is not within the ansible.modules package, disallow it. There are no
circumstances in which this is ok.

Change-Id: I7499e6b1091d745984ca36179de2793827c9f98f
This commit is contained in:
Monty Taylor 2017-08-02 16:14:05 -05:00
parent 3304ea9fda
commit 788a40e75c
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
17 changed files with 92 additions and 0 deletions

View File

@ -0,0 +1,3 @@
- hosts: localhost
roles:
- test-local-override

View File

@ -0,0 +1,31 @@
# Copyright (c) 2017 Red Hat
#
# This module 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.
#
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
# This file, by existing, should be found instead of ansible's built in
# file module.
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(required=False, type='str'),
state=dict(required=False, type='dict'),
)
)
module.exit_json(changed=False)
from ansible.module_utils.basic import * # noqa
from ansible.module_utils.basic import AnsibleModule

View File

@ -0,0 +1,4 @@
- name: Attempt to use local version of file.py
file:
path: some-file.out
state: touch

View File

@ -886,6 +886,7 @@ class TestAnsible(AnsibleZuulTestCase):
('csvfile_bad', 'FAILURE'),
('uri_bad_path', 'FAILURE'),
('uri_bad_scheme', 'FAILURE'),
('block_local_override', 'FAILURE'),
]
for job_name, result in plugin_tests:
count += 1

View File

@ -22,6 +22,9 @@ class ActionModule(assemble.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
source = self._task.args.get('src', None)
remote_src = self._task.args.get('remote_src', False)

View File

@ -22,6 +22,9 @@ class ActionModule(copy.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
source = self._task.args.get('src', None)
remote_src = self._task.args.get('remote_src', False)

View File

@ -21,6 +21,8 @@ fetch = paths._import_ansible_action_plugin("fetch")
class ActionModule(fetch.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
dest = self._task.args.get('dest', None)

View File

@ -21,6 +21,8 @@ include_vars = paths._import_ansible_action_plugin("include_vars")
class ActionModule(include_vars.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
source_dir = self._task.args.get('dir', None)
source_file = self._task.args.get('file', None)

View File

@ -50,6 +50,7 @@ class ActionModule(normal.ActionModule):
handler_name = 'handle_{action}'.format(action=self._task.action)
handler = getattr(self, handler_name, None)
if handler:
paths._fail_if_local_module(self._task.action)
handler()
return True
return False

View File

@ -21,6 +21,8 @@ patch = paths._import_ansible_action_plugin("patch")
class ActionModule(patch.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
source = self._task.args.get('src', None)
remote_src = self._task.args.get('remote_src', False)

View File

@ -22,6 +22,8 @@ class ActionModule(script.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
# the script name is the first item in the raw params, so we split it
# out now so we know the file name we need to transfer to the remote,
# and everything else is an argument to the script which we need later

View File

@ -21,6 +21,8 @@ synchronize = paths._import_ansible_action_plugin("synchronize")
class ActionModule(synchronize.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
source = self._task.args.get('src', None)
dest = self._task.args.get('dest', None)

View File

@ -21,6 +21,8 @@ template = paths._import_ansible_action_plugin("template")
class ActionModule(template.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
source = self._task.args.get('src', None)

View File

@ -21,6 +21,8 @@ unarchive = paths._import_ansible_action_plugin("unarchive")
class ActionModule(unarchive.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
source = self._task.args.get('src', None)
remote_src = self._task.args.get('remote_src', False)

View File

@ -21,6 +21,8 @@ win_copy = paths._import_ansible_action_plugin("win_copy")
class ActionModule(win_copy.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
source = self._task.args.get('src', None)
remote_src = self._task.args.get('remote_src', False)

View File

@ -21,6 +21,8 @@ win_template = paths._import_ansible_action_plugin("win_template")
class ActionModule(win_template.ActionModule):
def run(self, tmp=None, task_vars=None):
if not paths._is_official_module(self):
return paths._fail_module_dict(self._task.action)
source = self._task.args.get('src', None)
remote_src = self._task.args.get('remote_src', False)

View File

@ -17,6 +17,7 @@ import imp
import os
from ansible.errors import AnsibleError
import ansible.modules
import ansible.plugins.action
import ansible.plugins.lookup
@ -67,3 +68,30 @@ def _import_ansible_lookup_plugin(name):
return imp.load_module(
'zuul.ansible.protected.lookup.' + name,
*imp.find_module(name, ansible.plugins.lookup.__path__))
def _is_official_module(module):
task_module_path = module._shared_loader_obj.module_loader.find_plugin(
module._task.action)
ansible_module_path = os.path.dirname(ansible.modules.__file__)
# If the module is not beneath the main ansible library path that means
# someone has included a module with a playbook or a role that has the
# same name as one of the builtin modules. Normally we don't care, but for
# local execution it's a problem because their version could subvert our
# path checks and/or do other things on the local machine that we don't
# want them to do.
return task_module_path.startswith(ansible_module_path)
def _fail_module_dict(module_name):
return dict(
failed=True,
msg="Local execution of overridden module {name} is forbidden".format(
name=module_name))
def _fail_if_local_module(module_name):
if not _is_official_module(module_name):
msg_dict = _fail_module_dict(module_name)
raise AnsibleError(msg_dict['msg'])