CVE-2024-11079
Description
A flaw was found in Ansible-Core. This vulnerability allows attackers to bypass unsafe content protections using the hostvars object to reference and execute templated content. This issue can lead to arbitrary code execution if remote data or module outputs are improperly templated within playbooks.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Ansible-Core allows unsafe content bypass via hostvars, enabling potential code execution when templating remote data.
Vulnerability
Description CVE-2024-11079 is a flaw in Ansible-Core that allows attackers to bypass unsafe content protections by using the hostvars object to reference and execute templated content. This vulnerability stems from the templating engine not preferring AnsibleUnsafe when a variable is referenced via hostvars, as noted in the security fixes of Ansible-Core 2.18.1 [2].
Attack
Vector and Exploitation The issue is exploitable when remote data or module outputs are improperly templated within playbooks. An attacker who can influence data that is later used in templated expressions via hostvars can trigger the bypass, leading to arbitrary code execution. No authentication is required for exploitation if the attacker controls the data source [1][3].
Impact
Successful exploitation allows an attacker to achieve arbitrary code execution on the Ansible controller, potentially compromising the entire automation infrastructure. This could result in unauthorized access, data exfiltration, or further lateral movement within the managed environment [1][3].
Mitigation
Red Hat has released updates for Red Hat Ansible Automation Platform 2.5 and associated execution environments to address this vulnerability. Users should upgrade to ansible-core versions 2.15.13, 2.16.14, 2.17.7, or 2.18.1, depending on their stream [1][4]. The fix ensures that templating will not prefer AnsibleUnsafe when a variable is referenced via hostvars, closing the bypass [2].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ansible-corePyPI | >= 2.18.0b1, < 2.18.1rc1 | 2.18.1rc1 |
ansible-corePyPI | >= 2.17.0b1, < 2.17.7rc1 | 2.17.7rc1 |
ansible-corePyPI | < 2.16.14rc1 | 2.16.14rc1 |
Affected products
1Patches
370e83e72b43eFix CVE-2024-11079 hostvars unsafe context (#84339) (#84353)
6 files changed · +159 −37
changelogs/fragments/unsafe_hostvars_fix.yml+2 −0 added@@ -0,0 +1,2 @@ +security_fixes: + - Templating will not prefer AnsibleUnsafe when a variable is referenced via hostvars - CVE-2024-11079
lib/ansible/template/__init__.py+1 −30 modified@@ -49,7 +49,7 @@ 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.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.vars import AnsibleJ2Vars from ansible.utils.display import Display @@ -329,35 +329,6 @@ def wrapper(*args, **kwargs): return _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): ''' A custom context, which intercepts resolve_or_missing() calls and sets a flag
lib/ansible/template/native_helpers.py+118 −4 modified@@ -7,13 +7,19 @@ import ast +from collections.abc import Mapping from itertools import islice, chain 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.six import string_types from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode 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 = { @@ -30,6 +36,40 @@ def visit_Name(self, node): 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): """Return a string of concatenated compiled nodes. Throw an undefined error if any of the nodes is undefined. @@ -45,17 +85,28 @@ def ansible_eval_concat(nodes): if not head: return '' + unsafe = False + if len(head) == 1: out = head[0] if isinstance(out, NativeJinjaText): return out + unsafe = _is_unsafe(out) out = to_text(out) else: if isinstance(nodes, GeneratorType): 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 out.startswith(('{', '[')) or out in ('True', 'False'): @@ -70,6 +121,9 @@ def ansible_eval_concat(nodes): except (TypeError, ValueError, SyntaxError, MemoryError): pass + if unsafe: + out = wrap_var(out) + return out @@ -80,7 +134,19 @@ def ansible_concat(nodes): 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): @@ -97,6 +163,8 @@ def ansible_native_concat(nodes): if not head: return None + unsafe = False + if len(head) == 1: out = head[0] @@ -117,10 +185,21 @@ def ansible_native_concat(nodes): # short-circuit literal_eval for anything other than strings if not isinstance(out, string_types): return out + + unsafe = _is_unsafe(out) + else: if isinstance(nodes, GeneratorType): 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: evaled = ast.literal_eval( @@ -130,10 +209,45 @@ def ansible_native_concat(nodes): ast.parse(out, mode='eval') ) except (TypeError, ValueError, SyntaxError, MemoryError): + if unsafe: + out = wrap_var(out) + return out if isinstance(evaled, string_types): quote = out[0] - return f'{quote}{evaled}{quote}' + evaled = f'{quote}{evaled}{quote}' + + if unsafe: + evaled = wrap_var(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
lib/ansible/vars/hostvars.py+4 −3 modified@@ -94,11 +94,12 @@ def __contains__(self, host_name): return self._find_host(host_name) is not None def __iter__(self): - for host in self._inventory.hosts: - yield host + # 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): - 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): out = {}
test/integration/targets/template/cve-2024-11079.yml+30 −0 added@@ -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 }}'
test/integration/targets/template/runme.sh+4 −0 modified@@ -41,6 +41,10 @@ ansible-playbook 72262.yml -v "$@" # ensure unsafe is preserved, even with extra newlines 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 ansible-playbook template_overrides.yml -v "$@"
98774d15d774Fix CVE-2024-11079 hostvars unsafe context (#84339) (#84354)
6 files changed · +159 −36
changelogs/fragments/unsafe_hostvars_fix.yml+2 −0 added@@ -0,0 +1,2 @@ +security_fixes: + - Templating will not prefer AnsibleUnsafe when a variable is referenced via hostvars - CVE-2024-11079
lib/ansible/template/__init__.py+1 −30 modified@@ -48,7 +48,7 @@ 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.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.vars import AnsibleJ2Vars from ansible.utils.display import Display @@ -312,35 +312,6 @@ def wrapper(*args, **kwargs): 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): ''' A custom context, which intercepts resolve_or_missing() calls and sets a flag
lib/ansible/template/native_helpers.py+118 −4 modified@@ -5,13 +5,19 @@ import ast +from collections.abc import Mapping from itertools import islice, chain 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.six import string_types from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode 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 = { @@ -28,6 +34,40 @@ def visit_Name(self, node): 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): """Return a string of concatenated compiled nodes. Throw an undefined error if any of the nodes is undefined. @@ -43,17 +83,28 @@ def ansible_eval_concat(nodes): if not head: return '' + unsafe = False + if len(head) == 1: out = head[0] if isinstance(out, NativeJinjaText): return out + unsafe = _is_unsafe(out) out = to_text(out) else: if isinstance(nodes, GeneratorType): 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 out.startswith(('{', '[')) or out in ('True', 'False'): @@ -68,6 +119,9 @@ def ansible_eval_concat(nodes): except (TypeError, ValueError, SyntaxError, MemoryError): pass + if unsafe: + out = wrap_var(out) + return out @@ -78,7 +132,19 @@ def ansible_concat(nodes): 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): @@ -95,6 +161,8 @@ def ansible_native_concat(nodes): if not head: return None + unsafe = False + if len(head) == 1: out = head[0] @@ -115,10 +183,21 @@ def ansible_native_concat(nodes): # short-circuit literal_eval for anything other than strings if not isinstance(out, string_types): return out + + unsafe = _is_unsafe(out) + else: if isinstance(nodes, GeneratorType): 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: evaled = ast.literal_eval( @@ -128,10 +207,45 @@ def ansible_native_concat(nodes): ast.parse(out, mode='eval') ) except (TypeError, ValueError, SyntaxError, MemoryError): + if unsafe: + out = wrap_var(out) + return out if isinstance(evaled, string_types): quote = out[0] - return f'{quote}{evaled}{quote}' + evaled = f'{quote}{evaled}{quote}' + + if unsafe: + evaled = wrap_var(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
lib/ansible/vars/hostvars.py+4 −2 modified@@ -92,10 +92,12 @@ def __contains__(self, host_name): return self._find_host(host_name) is not None 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): - 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): out = {}
test/integration/targets/template/cve-2024-11079.yml+30 −0 added@@ -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 }}'
test/integration/targets/template/runme.sh+4 −0 modified@@ -41,6 +41,10 @@ ansible-playbook 72262.yml -v "$@" # ensure unsafe is preserved, even with extra newlines 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 ansible-playbook template_overrides.yml -v "$@"
2936b80dbbc7Fix CVE-2024-11079 hostvars unsafe context (#84339)
6 files changed · +159 −36
changelogs/fragments/unsafe_hostvars_fix.yml+2 −0 added@@ -0,0 +1,2 @@ +security_fixes: + - Templating will not prefer AnsibleUnsafe when a variable is referenced via hostvars - CVE-2024-11079
lib/ansible/template/__init__.py+1 −30 modified@@ -48,7 +48,7 @@ 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.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.vars import AnsibleJ2Vars from ansible.utils.display import Display @@ -312,35 +312,6 @@ def wrapper(*args, **kwargs): 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): ''' A custom context, which intercepts resolve_or_missing() calls and sets a flag
lib/ansible/template/native_helpers.py+118 −4 modified@@ -5,13 +5,19 @@ import ast +from collections.abc import Mapping from itertools import islice, chain 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.six import string_types from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode 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 = { @@ -28,6 +34,40 @@ def visit_Name(self, node): 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): """Return a string of concatenated compiled nodes. Throw an undefined error if any of the nodes is undefined. @@ -43,17 +83,28 @@ def ansible_eval_concat(nodes): if not head: return '' + unsafe = False + if len(head) == 1: out = head[0] if isinstance(out, NativeJinjaText): return out + unsafe = _is_unsafe(out) out = to_text(out) else: if isinstance(nodes, GeneratorType): 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 out.startswith(('{', '[')) or out in ('True', 'False'): @@ -68,6 +119,9 @@ def ansible_eval_concat(nodes): except (TypeError, ValueError, SyntaxError, MemoryError): pass + if unsafe: + out = wrap_var(out) + return out @@ -78,7 +132,19 @@ def ansible_concat(nodes): 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): @@ -95,6 +161,8 @@ def ansible_native_concat(nodes): if not head: return None + unsafe = False + if len(head) == 1: out = head[0] @@ -115,10 +183,21 @@ def ansible_native_concat(nodes): # short-circuit literal_eval for anything other than strings if not isinstance(out, string_types): return out + + unsafe = _is_unsafe(out) + else: if isinstance(nodes, GeneratorType): 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: evaled = ast.literal_eval( @@ -128,10 +207,45 @@ def ansible_native_concat(nodes): ast.parse(out, mode='eval') ) except (TypeError, ValueError, SyntaxError, MemoryError): + if unsafe: + out = wrap_var(out) + return out if isinstance(evaled, string_types): quote = out[0] - return f'{quote}{evaled}{quote}' + evaled = f'{quote}{evaled}{quote}' + + if unsafe: + evaled = wrap_var(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
lib/ansible/vars/hostvars.py+4 −2 modified@@ -92,10 +92,12 @@ def __contains__(self, host_name): return self._find_host(host_name) is not None 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): - 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): out = {}
test/integration/targets/template/cve-2024-11079.yml+30 −0 added@@ -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 }}'
test/integration/targets/template/runme.sh+4 −0 modified@@ -41,6 +41,10 @@ ansible-playbook 72262.yml -v "$@" # ensure unsafe is preserved, even with extra newlines 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 ansible-playbook template_overrides.yml -v "$@"
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
13- github.com/advisories/GHSA-99w6-3xph-cx78ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-11079ghsaADVISORY
- access.redhat.com/errata/RHSA-2024:10770nvdWEB
- access.redhat.com/errata/RHSA-2024:11145nvdWEB
- access.redhat.com/security/cve/CVE-2024-11079nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/ansible/ansible/blob/v2.18.1/changelogs/CHANGELOG-v2.18.rstghsaWEB
- github.com/ansible/ansible/commit/2936b80dbbc7efb889934aeec80f6142c10266ceghsaWEB
- github.com/ansible/ansible/commit/70e83e72b43e05e57eb42a6d52d01a4d9768f510ghsaWEB
- github.com/ansible/ansible/commit/98774d15d7748ebaaaf2e83942cc7e8d39f7280eghsaWEB
- github.com/ansible/ansible/pull/84299ghsaWEB
- github.com/ansible/ansible/pull/84339ghsaWEB
- lists.debian.org/debian-lts-announce/2026/03/msg00006.htmlnvdWEB
News mentions
0No linked articles in our index yet.