Browse Source

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
changes/54/553854/2
Tobias Henkel 3 years ago
parent
commit
9cbb681446
No known key found for this signature in database GPG Key ID: 3750DEC158E5FA2
25 changed files with 242 additions and 1 deletions
  1. +17
    -0
      tests/fixtures/config/speculative-plugins/git/common-config/zuul.yaml
  2. +1
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/README
  3. +14
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-bare-role/bare-role/filter_plugins/main.py
  4. +3
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-bare-role/bare-role/tasks/main.yaml
  5. +4
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-bare-role/test.yaml
  6. +1
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-playbook-symlink/filter_plugins
  7. +5
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-playbook-symlink/test.yaml
  8. +14
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-playbook/filter_plugins/main.py
  9. +5
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-playbook/test.yaml
  10. +14
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-role/roles/local-role/filter_plugins/main.py
  11. +3
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-role/roles/local-role/tasks/main.yaml
  12. +4
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-role/test.yaml
  13. +4
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-shared-bare-role/test.yaml
  14. +4
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-shared-role/test.yaml
  15. +4
    -0
      tests/fixtures/config/speculative-plugins/git/org_project/zuul.yaml
  16. +14
    -0
      tests/fixtures/config/speculative-plugins/git/org_project2/roles/shared-role/filter_plugins/main.py
  17. +3
    -0
      tests/fixtures/config/speculative-plugins/git/org_project2/roles/shared-role/tasks/main.yaml
  18. +14
    -0
      tests/fixtures/config/speculative-plugins/git/org_project3/filter_plugins/main.py
  19. +3
    -0
      tests/fixtures/config/speculative-plugins/git/org_project3/tasks/main.yaml
  20. +4
    -0
      tests/fixtures/config/speculative-plugins/git/org_projectrole/playbooks/filter-plugin-repo-role/test.yaml
  21. +14
    -0
      tests/fixtures/config/speculative-plugins/git/org_projectrole/roles/project-role/filter_plugins/main.py
  22. +3
    -0
      tests/fixtures/config/speculative-plugins/git/org_projectrole/roles/project-role/tasks/main.yaml
  23. +11
    -0
      tests/fixtures/config/speculative-plugins/main.yaml
  24. +45
    -0
      tests/unit/test_v3.py
  25. +34
    -1
      zuul/executor/server.py

+ 17
- 0
tests/fixtures/config/speculative-plugins/git/common-config/zuul.yaml 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

+ 1
- 0
tests/fixtures/config/speculative-plugins/git/org_project/README View File

@ -0,0 +1 @@
test

+ 14
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-bare-role/bare-role/filter_plugins/main.py 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
}

+ 3
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-bare-role/bare-role/tasks/main.yaml View File

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

+ 4
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-bare-role/test.yaml View File

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

+ 1
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-playbook-symlink/filter_plugins View File

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

+ 5
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-playbook-symlink/test.yaml View File

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

+ 14
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-playbook/filter_plugins/main.py 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
}

+ 5
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-playbook/test.yaml View File

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

+ 14
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-role/roles/local-role/filter_plugins/main.py 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
}

+ 3
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-role/roles/local-role/tasks/main.yaml View File

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

+ 4
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-role/test.yaml View File

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

+ 4
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-shared-bare-role/test.yaml View File

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

+ 4
- 0
tests/fixtures/config/speculative-plugins/git/org_project/playbooks/filter-plugin-shared-role/test.yaml View File

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

+ 4
- 0
tests/fixtures/config/speculative-plugins/git/org_project/zuul.yaml View File

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

+ 14
- 0
tests/fixtures/config/speculative-plugins/git/org_project2/roles/shared-role/filter_plugins/main.py 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
}

+ 3
- 0
tests/fixtures/config/speculative-plugins/git/org_project2/roles/shared-role/tasks/main.yaml View File

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

+ 14
- 0
tests/fixtures/config/speculative-plugins/git/org_project3/filter_plugins/main.py 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
}

+ 3
- 0
tests/fixtures/config/speculative-plugins/git/org_project3/tasks/main.yaml View File

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

+ 4
- 0
tests/fixtures/config/speculative-plugins/git/org_projectrole/playbooks/filter-plugin-repo-role/test.yaml View File

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

+ 14
- 0
tests/fixtures/config/speculative-plugins/git/org_projectrole/roles/project-role/filter_plugins/main.py 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
}

+ 3
- 0
tests/fixtures/config/speculative-plugins/git/org_projectrole/roles/project-role/tasks/main.yaml View File

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

+ 11
- 0
tests/fixtures/config/speculative-plugins/main.yaml 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

+ 45
- 0
tests/unit/test_v3.py 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'}]")

+ 34
- 1
zuul/executor/server.py 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)


Loading…
Cancel
Save