Skip to content

Commit

Permalink
Prevent RCE via inventory plugins.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Mar 12, 2024
1 parent 83d2d0e commit 963514a
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 50 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/inventory-rce.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
security_fixes:
- "docker_containers, docker_machine, and docker_swarm inventory plugins - make sure all data received from the Docker daemon / Docker machine is marked as unsafe, so remote code execution by obtaining texts that can be evaluated as templates is not possible (https://www.die-welt.net/2024/03/remote-code-execution-in-ansible-dynamic-inventory-plugins/, https://github.com/ansible-collections/community.docker/pull/815)."
9 changes: 6 additions & 3 deletions plugins/inventory/docker_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
from ansible.errors import AnsibleError
from ansible.module_utils.common.text.converters import to_native
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible.utils.unsafe_proxy import wrap_var as make_unsafe

from ansible_collections.community.docker.plugins.module_utils.common_api import (
RequestException,
Expand Down Expand Up @@ -239,8 +240,8 @@ def _populate(self, client):
full_name = id

facts = dict(
docker_name=name,
docker_short_id=short_id
docker_name=make_unsafe_var(name),
docker_short_id=make_unsafe_var(short_id),
)
full_facts = dict()

Expand Down Expand Up @@ -310,6 +311,8 @@ def _populate(self, client):
fact_key = self._slugify(key)
full_facts[fact_key] = value

full_facts = make_unsafe_var(full_facts)

if not filter_host(self, name, full_facts, filters):
continue

Expand All @@ -322,7 +325,7 @@ def _populate(self, client):
self.inventory.add_host(name, group=group)

for key, value in facts.items():
self.inventory.set_variable(name, key, value)
self.inventory.set_variable(name, key, make_unsafe_var(value))

# Use constructed if applicable
# Composed variables
Expand Down
45 changes: 24 additions & 21 deletions plugins/inventory/docker_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
from ansible.module_utils.common.process import get_bin_path
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.utils.display import Display
from ansible.utils.unsafe_proxy import wrap_var as make_unsafe

from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import parse_filters, filter_host

Expand Down Expand Up @@ -178,15 +179,15 @@ def _get_machine_names(self):

def _inspect_docker_machine_host(self, node):
try:
inspect_lines = self._run_command(['inspect', self.node])
inspect_lines = self._run_command(['inspect', node])
except subprocess.CalledProcessError:
return None

return json.loads(inspect_lines)

def _ip_addr_docker_machine_host(self, node):
try:
ip_addr = self._run_command(['ip', self.node])
ip_addr = self._run_command(['ip', node])
except subprocess.CalledProcessError:
return None

Expand All @@ -208,13 +209,15 @@ def _populate(self):
daemon_env = self.get_option('daemon_env')
filters = parse_filters(self.get_option('filters'))
try:
for self.node in self._get_machine_names():
self.node_attrs = self._inspect_docker_machine_host(self.node)
if not self.node_attrs:
for node in self._get_machine_names():
node_attrs = self._inspect_docker_machine_host(node)
if not node_attrs:
continue

machine_name = self.node_attrs['Driver']['MachineName']
if not filter_host(self, machine_name, self.node_attrs, filters):
unsafe_node_attrs = make_unsafe(node_attrs)

machine_name = unsafe_node_attrs['Driver']['MachineName']
if not filter_host(self, machine_name, unsafe_node_attrs, filters):
continue

# query `docker-machine env` to obtain remote Docker daemon connection settings in the form of commands
Expand All @@ -232,40 +235,40 @@ def _populate(self):
# check for valid ip address from inspect output, else explicitly use ip command to find host ip address
# this works around an issue seen with Google Compute Platform where the IP address was not available
# via the 'inspect' subcommand but was via the 'ip' subcomannd.
if self.node_attrs['Driver']['IPAddress']:
ip_addr = self.node_attrs['Driver']['IPAddress']
if unsafe_node_attrs['Driver']['IPAddress']:
ip_addr = unsafe_node_attrs['Driver']['IPAddress']
else:
ip_addr = self._ip_addr_docker_machine_host(self.node)
ip_addr = self._ip_addr_docker_machine_host(node)

# set standard Ansible remote host connection settings to details captured from `docker-machine`
# see: https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html
self.inventory.set_variable(machine_name, 'ansible_host', ip_addr)
self.inventory.set_variable(machine_name, 'ansible_port', self.node_attrs['Driver']['SSHPort'])
self.inventory.set_variable(machine_name, 'ansible_user', self.node_attrs['Driver']['SSHUser'])
self.inventory.set_variable(machine_name, 'ansible_ssh_private_key_file', self.node_attrs['Driver']['SSHKeyPath'])
self.inventory.set_variable(machine_name, 'ansible_host', make_unsafe(ip_addr))
self.inventory.set_variable(machine_name, 'ansible_port', unsafe_node_attrs['Driver']['SSHPort'])
self.inventory.set_variable(machine_name, 'ansible_user', unsafe_node_attrs['Driver']['SSHUser'])
self.inventory.set_variable(machine_name, 'ansible_ssh_private_key_file', unsafe_node_attrs['Driver']['SSHKeyPath'])

# set variables based on Docker Machine tags
tags = self.node_attrs['Driver'].get('Tags') or ''
self.inventory.set_variable(machine_name, 'dm_tags', tags)
tags = unsafe_node_attrs['Driver'].get('Tags') or ''
self.inventory.set_variable(machine_name, 'dm_tags', make_unsafe(tags))

# set variables based on Docker Machine env variables
for kv in env_var_tuples:
self.inventory.set_variable(machine_name, 'dm_{0}'.format(kv[0]), kv[1])
self.inventory.set_variable(machine_name, 'dm_{0}'.format(kv[0]), make_unsafe(kv[1]))

if self.get_option('verbose_output'):
self.inventory.set_variable(machine_name, 'docker_machine_node_attributes', self.node_attrs)
self.inventory.set_variable(machine_name, 'docker_machine_node_attributes', unsafe_node_attrs)

# Use constructed if applicable
strict = self.get_option('strict')

# Composed variables
self._set_composite_vars(self.get_option('compose'), self.node_attrs, machine_name, strict=strict)
self._set_composite_vars(self.get_option('compose'), unsafe_node_attrs, machine_name, strict=strict)

# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
self._add_host_to_composed_groups(self.get_option('groups'), self.node_attrs, machine_name, strict=strict)
self._add_host_to_composed_groups(self.get_option('groups'), unsafe_node_attrs, machine_name, strict=strict)

# Create groups based on variable values and add the corresponding hosts to it
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), self.node_attrs, machine_name, strict=strict)
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), unsafe_node_attrs, machine_name, strict=strict)

except Exception as e:
raise AnsibleError('Unable to fetch hosts from Docker Machine, this was the original exception: %s' %
Expand Down
54 changes: 28 additions & 26 deletions plugins/inventory/docker_swarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
from ansible_collections.community.docker.plugins.module_utils.util import update_tls_hostname
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible.parsing.utils.addresses import parse_address
from ansible.utils.unsafe_proxy import wrap_var as make_unsafe

from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import parse_filters, filter_host

Expand Down Expand Up @@ -213,50 +214,51 @@ def _populate(self):

try:
self.nodes = self.client.nodes.list()
for self.node in self.nodes:
self.node_attrs = self.client.nodes.get(self.node.id).attrs
if not filter_host(self, self.node_attrs['ID'], self.node_attrs, filters):
for node in self.nodes:
node_attrs = self.client.nodes.get(node.id).attrs
unsafe_node_attrs = make_unsafe(node_attrs)
if not filter_host(self, unsafe_node_attrs['ID'], unsafe_node_attrs, filters):
continue
self.inventory.add_host(self.node_attrs['ID'])
self.inventory.add_host(self.node_attrs['ID'], group=self.node_attrs['Spec']['Role'])
self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host',
self.node_attrs['Status']['Addr'])
self.inventory.add_host(unsafe_node_attrs['ID'])
self.inventory.add_host(unsafe_node_attrs['ID'], group=unsafe_node_attrs['Spec']['Role'])
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host',
unsafe_node_attrs['Status']['Addr'])
if self.get_option('include_host_uri'):
self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host_uri',
'tcp://' + self.node_attrs['Status']['Addr'] + ':' + host_uri_port)
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host_uri',
make_unsafe('tcp://' + unsafe_node_attrs['Status']['Addr'] + ':' + host_uri_port))
if self.get_option('verbose_output'):
self.inventory.set_variable(self.node_attrs['ID'], 'docker_swarm_node_attributes', self.node_attrs)
if 'ManagerStatus' in self.node_attrs:
if self.node_attrs['ManagerStatus'].get('Leader'):
self.inventory.set_variable(unsafe_node_attrs['ID'], 'docker_swarm_node_attributes', unsafe_node_attrs)
if 'ManagerStatus' in unsafe_node_attrs:
if unsafe_node_attrs['ManagerStatus'].get('Leader'):
# This is workaround of bug in Docker when in some cases the Leader IP is 0.0.0.0
# Check moby/moby#35437 for details
swarm_leader_ip = parse_address(self.node_attrs['ManagerStatus']['Addr'])[0] or \
self.node_attrs['Status']['Addr']
swarm_leader_ip = parse_address(node_attrs['ManagerStatus']['Addr'])[0] or \
unsafe_node_attrs['Status']['Addr']
if self.get_option('include_host_uri'):
self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host_uri',
'tcp://' + swarm_leader_ip + ':' + host_uri_port)
self.inventory.set_variable(self.node_attrs['ID'], 'ansible_host', swarm_leader_ip)
self.inventory.add_host(self.node_attrs['ID'], group='leader')
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host_uri',
make_unsafe('tcp://' + swarm_leader_ip + ':' + host_uri_port))
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host', make_unsafe(swarm_leader_ip))
self.inventory.add_host(unsafe_node_attrs['ID'], group='leader')
else:
self.inventory.add_host(self.node_attrs['ID'], group='nonleaders')
self.inventory.add_host(unsafe_node_attrs['ID'], group='nonleaders')
else:
self.inventory.add_host(self.node_attrs['ID'], group='nonleaders')
self.inventory.add_host(unsafe_node_attrs['ID'], group='nonleaders')
# Use constructed if applicable
strict = self.get_option('strict')
# Composed variables
self._set_composite_vars(self.get_option('compose'),
self.node_attrs,
self.node_attrs['ID'],
unsafe_node_attrs,
unsafe_node_attrs['ID'],
strict=strict)
# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
self._add_host_to_composed_groups(self.get_option('groups'),
self.node_attrs,
self.node_attrs['ID'],
unsafe_node_attrs,
unsafe_node_attrs['ID'],
strict=strict)
# Create groups based on variable values and add the corresponding hosts to it
self._add_host_to_keyed_groups(self.get_option('keyed_groups'),
self.node_attrs,
self.node_attrs['ID'],
unsafe_node_attrs,
unsafe_node_attrs['ID'],
strict=strict)
except Exception as e:
raise AnsibleError('Unable to fetch hosts from Docker swarm API, this was the original exception: %s' %
Expand Down

0 comments on commit 963514a

Please sign in to comment.