Fix CVE-2024-11079 hostvars unsafe context (#84339) (#84354)

Fix to preserve an unsafe variable when accessing through an
intermediary variable from hostvars.

(cherry picked from commit 2936b80dbb)
stable-2.17
Jordan Borean 1 day ago committed by GitHub
parent 1c86e75227
commit 98774d15d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,2 @@
security_fixes:
- Templating will not prefer AnsibleUnsafe when a variable is referenced via hostvars - CVE-2024-11079

@ -48,7 +48,7 @@ from ansible.module_utils.six import string_types
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
from ansible.module_utils.common.collections import is_sequence from ansible.module_utils.common.collections import is_sequence
from ansible.plugins.loader import filter_loader, lookup_loader, test_loader from ansible.plugins.loader import filter_loader, lookup_loader, test_loader
from ansible.template.native_helpers import ansible_native_concat, ansible_eval_concat, ansible_concat from ansible.template.native_helpers import AnsibleUndefined, ansible_native_concat, ansible_eval_concat, ansible_concat
from ansible.template.template import AnsibleJ2Template from ansible.template.template import AnsibleJ2Template
from ansible.template.vars import AnsibleJ2Vars from ansible.template.vars import AnsibleJ2Vars
from ansible.utils.display import Display from ansible.utils.display import Display
@ -312,35 +312,6 @@ def _wrap_native_text(func):
return functools.update_wrapper(wrapper, func) return functools.update_wrapper(wrapper, func)
class AnsibleUndefined(StrictUndefined):
'''
A custom Undefined class, which returns further Undefined objects on access,
rather than throwing an exception.
'''
def __getattr__(self, name):
if name == '__UNSAFE__':
# AnsibleUndefined should never be assumed to be unsafe
# This prevents ``hasattr(val, '__UNSAFE__')`` from evaluating to ``True``
raise AttributeError(name)
# Return original Undefined object to preserve the first failure context
return self
def __getitem__(self, key):
# Return original Undefined object to preserve the first failure context
return self
def __repr__(self):
return 'AnsibleUndefined(hint={0!r}, obj={1!r}, name={2!r})'.format(
self._undefined_hint,
self._undefined_obj,
self._undefined_name
)
def __contains__(self, item):
# Return original Undefined object to preserve the first failure context
return self
class AnsibleContext(Context): class AnsibleContext(Context):
''' '''
A custom context, which intercepts resolve_or_missing() calls and sets a flag A custom context, which intercepts resolve_or_missing() calls and sets a flag

@ -5,13 +5,19 @@ from __future__ import annotations
import ast import ast
from collections.abc import Mapping
from itertools import islice, chain from itertools import islice, chain
from types import GeneratorType from types import GeneratorType
from ansible.module_utils.common.collections import is_sequence
from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
from ansible.utils.native_jinja import NativeJinjaText from ansible.utils.native_jinja import NativeJinjaText
from ansible.utils.unsafe_proxy import wrap_var
import ansible.module_utils.compat.typing as t
from jinja2.runtime import StrictUndefined
_JSON_MAP = { _JSON_MAP = {
@ -28,6 +34,40 @@ class Json2Python(ast.NodeTransformer):
return ast.Constant(value=_JSON_MAP[node.id]) return ast.Constant(value=_JSON_MAP[node.id])
def _is_unsafe(value: t.Any) -> bool:
"""
Our helper function, which will also recursively check dict and
list entries due to the fact that they may be repr'd and contain
a key or value which contains jinja2 syntax and would otherwise
lose the AnsibleUnsafe value.
"""
to_check = [value]
seen = set()
while True:
if not to_check:
break
val = to_check.pop(0)
val_id = id(val)
if val_id in seen:
continue
seen.add(val_id)
if isinstance(val, AnsibleUndefined):
continue
if isinstance(val, Mapping):
to_check.extend(val.keys())
to_check.extend(val.values())
elif is_sequence(val):
to_check.extend(val)
elif getattr(val, '__UNSAFE__', False):
return True
return False
def ansible_eval_concat(nodes): def ansible_eval_concat(nodes):
"""Return a string of concatenated compiled nodes. Throw an undefined error """Return a string of concatenated compiled nodes. Throw an undefined error
if any of the nodes is undefined. if any of the nodes is undefined.
@ -43,17 +83,28 @@ def ansible_eval_concat(nodes):
if not head: if not head:
return '' return ''
unsafe = False
if len(head) == 1: if len(head) == 1:
out = head[0] out = head[0]
if isinstance(out, NativeJinjaText): if isinstance(out, NativeJinjaText):
return out return out
unsafe = _is_unsafe(out)
out = to_text(out) out = to_text(out)
else: else:
if isinstance(nodes, GeneratorType): if isinstance(nodes, GeneratorType):
nodes = chain(head, nodes) nodes = chain(head, nodes)
out = ''.join([to_text(v) for v in nodes])
out_values = []
for v in nodes:
if not unsafe and _is_unsafe(v):
unsafe = True
out_values.append(to_text(v))
out = ''.join(out_values)
# if this looks like a dictionary, list or bool, convert it to such # if this looks like a dictionary, list or bool, convert it to such
if out.startswith(('{', '[')) or out in ('True', 'False'): if out.startswith(('{', '[')) or out in ('True', 'False'):
@ -68,6 +119,9 @@ def ansible_eval_concat(nodes):
except (TypeError, ValueError, SyntaxError, MemoryError): except (TypeError, ValueError, SyntaxError, MemoryError):
pass pass
if unsafe:
out = wrap_var(out)
return out return out
@ -78,7 +132,19 @@ def ansible_concat(nodes):
Used in Templar.template() when jinja2_native=False and convert_data=False. Used in Templar.template() when jinja2_native=False and convert_data=False.
""" """
return ''.join([to_text(v) for v in nodes]) unsafe = False
values = []
for v in nodes:
if not unsafe and _is_unsafe(v):
unsafe = True
values.append(to_text(v))
out = ''.join(values)
if unsafe:
out = wrap_var(out)
return out
def ansible_native_concat(nodes): def ansible_native_concat(nodes):
@ -95,6 +161,8 @@ def ansible_native_concat(nodes):
if not head: if not head:
return None return None
unsafe = False
if len(head) == 1: if len(head) == 1:
out = head[0] out = head[0]
@ -115,10 +183,21 @@ def ansible_native_concat(nodes):
# short-circuit literal_eval for anything other than strings # short-circuit literal_eval for anything other than strings
if not isinstance(out, string_types): if not isinstance(out, string_types):
return out return out
unsafe = _is_unsafe(out)
else: else:
if isinstance(nodes, GeneratorType): if isinstance(nodes, GeneratorType):
nodes = chain(head, nodes) nodes = chain(head, nodes)
out = ''.join([to_text(v) for v in nodes])
out_values = []
for v in nodes:
if not unsafe and _is_unsafe(v):
unsafe = True
out_values.append(to_text(v))
out = ''.join(out_values)
try: try:
evaled = ast.literal_eval( evaled = ast.literal_eval(
@ -128,10 +207,45 @@ def ansible_native_concat(nodes):
ast.parse(out, mode='eval') ast.parse(out, mode='eval')
) )
except (TypeError, ValueError, SyntaxError, MemoryError): except (TypeError, ValueError, SyntaxError, MemoryError):
if unsafe:
out = wrap_var(out)
return out return out
if isinstance(evaled, string_types): if isinstance(evaled, string_types):
quote = out[0] quote = out[0]
return f'{quote}{evaled}{quote}' evaled = f'{quote}{evaled}{quote}'
if unsafe:
evaled = wrap_var(evaled)
return evaled return evaled
class AnsibleUndefined(StrictUndefined):
"""
A custom Undefined class, which returns further Undefined objects on access,
rather than throwing an exception.
"""
def __getattr__(self, name):
if name == '__UNSAFE__':
# AnsibleUndefined should never be assumed to be unsafe
# This prevents ``hasattr(val, '__UNSAFE__')`` from evaluating to ``True``
raise AttributeError(name)
# Return original Undefined object to preserve the first failure context
return self
def __getitem__(self, key):
# Return original Undefined object to preserve the first failure context
return self
def __repr__(self):
return 'AnsibleUndefined(hint={0!r}, obj={1!r}, name={2!r})'.format(
self._undefined_hint,
self._undefined_obj,
self._undefined_name
)
def __contains__(self, item):
# Return original Undefined object to preserve the first failure context
return self

@ -92,10 +92,12 @@ class HostVars(Mapping):
return self._find_host(host_name) is not None return self._find_host(host_name) is not None
def __iter__(self): def __iter__(self):
yield from self._inventory.hosts # include implicit localhost only if it has variables set
yield from self._inventory.hosts | {'localhost': self._inventory.localhost} if self._inventory.localhost else {}
def __len__(self): def __len__(self):
return len(self._inventory.hosts) # include implicit localhost only if it has variables set
return len(self._inventory.hosts) + (1 if self._inventory.localhost else 0)
def __repr__(self): def __repr__(self):
out = {} out = {}

@ -0,0 +1,30 @@
- name: test CVE-2024-11079 loop variables preserve unsafe hostvars
hosts: localhost
gather_facts: false
tasks:
- set_fact:
foo:
safe:
prop: '{{ "{{" }} unsafe_var {{ "}}" }}'
unsafe:
prop: !unsafe '{{ unsafe_var }}'
- name: safe var through hostvars loop is templated
assert:
that:
- item.prop == expected
loop:
- "{{ hostvars['localhost']['foo']['safe'] }}"
vars:
unsafe_var: bar
expected: bar
- name: unsafe var through hostvars loop is not templated
assert:
that:
- item.prop == expected
loop:
- "{{ hostvars['localhost']['foo']['unsafe'] }}"
vars:
unsafe_var: bar
expected: !unsafe '{{ unsafe_var }}'

@ -41,6 +41,10 @@ ansible-playbook 72262.yml -v "$@"
# ensure unsafe is preserved, even with extra newlines # ensure unsafe is preserved, even with extra newlines
ansible-playbook unsafe.yml -v "$@" ansible-playbook unsafe.yml -v "$@"
# CVE 2024-11079
ANSIBLE_JINJA2_NATIVE=true ansible-playbook cve-2024-11079.yml -v "$@"
ANSIBLE_JINJA2_NATIVE=false ansible-playbook cve-2024-11079.yml -v "$@"
# ensure Jinja2 overrides from a template are used # ensure Jinja2 overrides from a template are used
ansible-playbook template_overrides.yml -v "$@" ansible-playbook template_overrides.yml -v "$@"

Loading…
Cancel
Save