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:
parent
cb4b10f517
commit
9cbb681446
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
test
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
- name: Test filter plugin
|
||||
debug:
|
||||
msg: "{{ 'ignore me' | my_cool_test }}"
|
|
@ -0,0 +1,4 @@
|
|||
- hosts: all
|
||||
roles:
|
||||
- bare-role
|
||||
|
|
@ -0,0 +1 @@
|
|||
symlink: ../filter-plugin-playbook/filter_plugins
|
|
@ -0,0 +1,5 @@
|
|||
- hosts: all
|
||||
tasks:
|
||||
- name: Test filter plugin
|
||||
debug:
|
||||
msg: "{{ 'ignore me' | my_cool_test }}"
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
- hosts: all
|
||||
tasks:
|
||||
- name: Test filter plugin
|
||||
debug:
|
||||
msg: "{{ 'ignore me' | my_cool_test }}"
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
- name: Test filter plugin
|
||||
debug:
|
||||
msg: "{{ 'ignore me' | my_cool_test }}"
|
|
@ -0,0 +1,4 @@
|
|||
- hosts: all
|
||||
roles:
|
||||
- local-role
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
- hosts: all
|
||||
roles:
|
||||
- shared-bare-role
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
- hosts: all
|
||||
roles:
|
||||
- shared-role
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
- project:
|
||||
check:
|
||||
jobs:
|
||||
- noop
|
|
@ -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
|
||||
}
|
3
tests/fixtures/config/speculative-plugins/git/org_project2/roles/shared-role/tasks/main.yaml
vendored
Normal file
3
tests/fixtures/config/speculative-plugins/git/org_project2/roles/shared-role/tasks/main.yaml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
- name: Test filter plugin
|
||||
debug:
|
||||
msg: "{{ 'ignore me' | my_cool_test }}"
|
14
tests/fixtures/config/speculative-plugins/git/org_project3/filter_plugins/main.py
vendored
Normal file
14
tests/fixtures/config/speculative-plugins/git/org_project3/filter_plugins/main.py
vendored
Normal 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
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
- name: Test filter plugin
|
||||
debug:
|
||||
msg: "{{ 'ignore me' | my_cool_test }}"
|
|
@ -0,0 +1,4 @@
|
|||
- hosts: all
|
||||
roles:
|
||||
- project-role
|
||||
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
- name: Test filter plugin
|
||||
debug:
|
||||
msg: "{{ 'ignore me' | my_cool_test }}"
|
|
@ -0,0 +1,11 @@
|
|||
- tenant:
|
||||
name: tenant-one
|
||||
source:
|
||||
gerrit:
|
||||
config-projects:
|
||||
- common-config
|
||||
untrusted-projects:
|
||||
- org/project
|
||||
- org/project2
|
||||
- org/project3
|
||||
- org/projectrole
|
|
@ -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'}]")
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue