Fix plugin injection vulnerability

Currently it is possible to inject speculative plugins into untrusted
jobs. These plugins are run locally on the executor and make it
possible to run arbitraty code within the bwrap context.

There are two problems here. First the path check is broken such it
never matches a plugin dir. Further we don't check paths residing
within playbook dirs.

Change-Id: Idf1b940de2be7819afeb2dbad943fad2ae7ebc55
This commit is contained in:
Tobias Henkel 2018-03-14 09:19:52 +01:00
parent cb4b10f517
commit 9cbb681446
No known key found for this signature in database
GPG Key ID: 03750DEC158E5FA2
25 changed files with 242 additions and 1 deletions

View File

@ -0,0 +1,17 @@
- pipeline:
name: check
manager: independent
post-review: true
trigger:
gerrit:
- event: patchset-created
success:
gerrit:
Verified: 1
failure:
gerrit:
Verified: -1
- job:
name: base
parent: null

View File

@ -0,0 +1 @@
test

View File

@ -0,0 +1,14 @@
import subprocess
def my_cool_test(string):
shell_output = subprocess.check_output(['hostname'])
return 'hostname: %s' % shell_output.decode('utf-8')
class FilterModule(object):
def filters(self):
return {
'my_cool_test': my_cool_test
}

View File

@ -0,0 +1,3 @@
- name: Test filter plugin
debug:
msg: "{{ 'ignore me' | my_cool_test }}"

View File

@ -0,0 +1,4 @@
- hosts: all
roles:
- bare-role

View File

@ -0,0 +1 @@
symlink: ../filter-plugin-playbook/filter_plugins

View File

@ -0,0 +1,5 @@
- hosts: all
tasks:
- name: Test filter plugin
debug:
msg: "{{ 'ignore me' | my_cool_test }}"

View File

@ -0,0 +1,14 @@
import subprocess
def my_cool_test(string):
shell_output = subprocess.check_output(['hostname'])
return 'hostname: %s' % shell_output.decode('utf-8')
class FilterModule(object):
def filters(self):
return {
'my_cool_test': my_cool_test
}

View File

@ -0,0 +1,5 @@
- hosts: all
tasks:
- name: Test filter plugin
debug:
msg: "{{ 'ignore me' | my_cool_test }}"

View File

@ -0,0 +1,14 @@
import subprocess
def my_cool_test(string):
shell_output = subprocess.check_output(['hostname'])
return 'hostname: %s' % shell_output.decode('utf-8')
class FilterModule(object):
def filters(self):
return {
'my_cool_test': my_cool_test
}

View File

@ -0,0 +1,3 @@
- name: Test filter plugin
debug:
msg: "{{ 'ignore me' | my_cool_test }}"

View File

@ -0,0 +1,4 @@
- hosts: all
roles:
- local-role

View File

@ -0,0 +1,4 @@
- hosts: all
roles:
- shared-bare-role

View File

@ -0,0 +1,4 @@
- hosts: all
roles:
- shared-role

View File

@ -0,0 +1,4 @@
- project:
check:
jobs:
- noop

View File

@ -0,0 +1,14 @@
import subprocess
def my_cool_test(string):
shell_output = subprocess.check_output(['hostname'])
return 'hostname: %s' % shell_output.decode('utf-8')
class FilterModule(object):
def filters(self):
return {
'my_cool_test': my_cool_test
}

View File

@ -0,0 +1,3 @@
- name: Test filter plugin
debug:
msg: "{{ 'ignore me' | my_cool_test }}"

View File

@ -0,0 +1,14 @@
import subprocess
def my_cool_test(string):
shell_output = subprocess.check_output(['hostname'])
return 'hostname: %s' % shell_output.decode('utf-8')
class FilterModule(object):
def filters(self):
return {
'my_cool_test': my_cool_test
}

View File

@ -0,0 +1,3 @@
- name: Test filter plugin
debug:
msg: "{{ 'ignore me' | my_cool_test }}"

View File

@ -0,0 +1,4 @@
- hosts: all
roles:
- project-role

View File

@ -0,0 +1,14 @@
import subprocess
def my_cool_test(string):
shell_output = subprocess.check_output(['hostname'])
return 'hostname: %s' % shell_output.decode('utf-8')
class FilterModule(object):
def filters(self):
return {
'my_cool_test': my_cool_test
}

View File

@ -0,0 +1,3 @@
- name: Test filter plugin
debug:
msg: "{{ 'ignore me' | my_cool_test }}"

View File

@ -0,0 +1,11 @@
- tenant:
name: tenant-one
source:
gerrit:
config-projects:
- common-config
untrusted-projects:
- org/project
- org/project2
- org/project3
- org/projectrole

View File

@ -3429,3 +3429,48 @@ class TestJobOutput(AnsibleZuulTestCase):
log_output = output.getvalue()
self.assertIn('Final playbook failed', log_output)
self.assertIn('Failure test', log_output)
class TestPlugins(AnsibleZuulTestCase):
tenant_config_file = 'config/speculative-plugins/main.yaml'
def _run_job(self, job_name, project='org/project', roles=''):
# Output extra ansible info so we might see errors.
self.executor_server.verbose = True
conf = textwrap.dedent(
"""
- job:
name: {job_name}
run: playbooks/{job_name}/test.yaml
nodeset:
nodes:
- name: controller
label: whatever
{roles}
- project:
check:
jobs:
- {job_name}
""".format(job_name=job_name, roles=roles))
file_dict = {'zuul.yaml': conf}
A = self.fake_gerrit.addFakeChange(project, 'master', 'A',
files=file_dict)
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
self.waitUntilSettled()
message = A.messages[0]
self.assertIn('ERROR Ansible plugin dir', message)
self.assertIn('found adjacent to playbook', message)
self.assertIn('in non-trusted repo', message)
def test_filter_plugin(self):
self._run_job('filter-plugin-playbook')
self._run_job('filter-plugin-playbook-symlink')
self._run_job('filter-plugin-bare-role')
self._run_job('filter-plugin-role')
self._run_job('filter-plugin-repo-role', project='org/projectrole')
self._run_job('filter-plugin-shared-role',
roles="roles: [{zuul: 'org/project2'}]")
self._run_job('filter-plugin-shared-bare-role',
roles="roles: [{zuul: 'org/project3', name: 'shared'}]")

View File

@ -1028,6 +1028,7 @@ class AnsibleJob(object):
'''
for entry in os.listdir(path):
entry = os.path.join(path, entry)
if os.path.isdir(entry) and entry.endswith('_plugins'):
raise ExecutorError(
"Ansible plugin dir %s found adjacent to playbook %s in "
@ -1036,8 +1037,40 @@ class AnsibleJob(object):
def findPlaybook(self, path, trusted=False):
if os.path.exists(path):
if not trusted:
# Plugins can be defined in multiple locations within the
# playbook's subtree.
#
# 1. directly within the playbook:
# block playbook_dir/*_plugins
#
# 2. within a role defined in playbook_dir/<rolename>:
# block playbook_dir/*/*_plugins
#
# 3. within a role defined in playbook_dir/roles/<rolename>:
# block playbook_dir/roles/*/*_plugins
playbook_dir = os.path.dirname(os.path.abspath(path))
self._blockPluginDirs(playbook_dir)
paths_to_check = []
def addPathsToCheck(root_dir):
if os.path.isdir(root_dir):
for entry in os.listdir(root_dir):
entry = os.path.join(root_dir, entry)
if os.path.isdir(entry):
paths_to_check.append(entry)
# handle case 1
paths_to_check.append(playbook_dir)
# handle case 2
addPathsToCheck(playbook_dir)
# handle case 3
addPathsToCheck(os.path.join(playbook_dir, 'roles'))
for path_to_check in paths_to_check:
self._blockPluginDirs(path_to_check)
return path
raise ExecutorError("Unable to find playbook %s" % path)