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:
parent
3304ea9fda
commit
788a40e75c
3
tests/fixtures/config/ansible/git/org_plugin-project/playbooks/block_local_override.yaml
vendored
Normal file
3
tests/fixtures/config/ansible/git/org_plugin-project/playbooks/block_local_override.yaml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
- hosts: localhost
|
||||
roles:
|
||||
- test-local-override
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
- name: Attempt to use local version of file.py
|
||||
file:
|
||||
path: some-file.out
|
||||
state: touch
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'])
|
||||
|
|
Loading…
Reference in New Issue