-
Notifications
You must be signed in to change notification settings - Fork 199
AnsibleUnsafe notes
Alex Willmer edited this page Mar 25, 2024
·
7 revisions
ansible.utils.unsafe_proxy.AnsibleUnsafe
is a mechanism to avoid
template injection attacks. Values marked unsafe should not be
(recursively) evaluted by Jinja2, doing so risks giving an attacker
arbitrary code execution.
At time of writing (March 2024) sub-classes of AnsibleUnsafe
are
AnsibleUnsafeBytes
AnsibleUnsafeText
NativeJinjaUnsafeText
Values considered unsafe generally come from untrusted/external sources, e.g.
- result of any lookup
{{ lookup('env', ...) }}
- result of any module executed on a target, e.g.
command
,template
- explicitly tagged in YAML,
may_contain_braces: !unsafe "{{"
Values are usually marked by calling
ansible.utils.unsafe_proxy.wrap_var()
, which recursively walks
sequences/mappings, replacing strings & byte strings.
Origin | Where unsafe is applied |
---|---|
LookupPlugin |
ansible.templates.Templar._lookup() |
AnsibleModule |
ansible.plugins.action.ActionBase._execute_module() |
$ ansible localhost -e 'answer=42' -m debug -a 'msg={{ answer | type_debug }}' localhost | SUCCESS => { "msg": "str" } $ ansible localhost -e 'env_home={{ lookup("env", "HOME") }}' \ -m debug -a 'msg={{ env_home | type_debug }}' localhost | SUCCESS => { "msg": "AnsibleUnsafeText" }
Modify ansible/utils/unsafe_proxy.py
to raise an exception given a
sentinal value you can generate, e.g.
diff --git a/utils/unsafe_proxy.py b/utils/unsafe_proxy.py
index d5816ad..1e43966 100644
--- a/utils/unsafe_proxy.py
+++ b/utils/unsafe_proxy.py
@@ -201,6 +201,11 @@ class AnsibleUnsafeBytes(bytes, AnsibleUnsafe):
class AnsibleUnsafeText(str, AnsibleUnsafe):
+ def __new__(cls, *args, **kwargs):
+ s = str(*args, **kwargs)
+ if s == 'rumplestiltskin': raise RuntimeError
+ return super().__new__(cls, *args, **kwargs)
+
def _strip_unsafe(self, /):
return super().__str__()
Run Ansible with high verbosity (-vvv
) to see the resulting traceback
$ ansible localhost -mcommand -a"echo rumplestiltskin" -vvv ... The full traceback is: Traceback (most recent call last): File ".../ansible/executor/task_executor.py", line 158, in run res = self._execute() ^^^^^^^^^^^^^^^ File ".../ansible/executor/task_executor.py", line 633, in _execute result = self._handler.run(task_vars=vars_copy) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ".../ansible/plugins/action/command.py", line 22, in run results = merge_hash(results, self._execute_module(module_name='ansible.legacy.command', task_vars=task_vars, wrap_async=wrap_async)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ".../ansible/plugins/action/__init__.py", line 1223, in _execute_module data = wrap_var(data) ^^^^^^^^^^^^^^ File ".../ansible/utils/unsafe_proxy.py", line 370, in wrap_var v = _wrap_dict(v) ^^^^^^^^^^^^^ File ".../ansible/utils/unsafe_proxy.py", line 350, in _wrap_dict return dict((wrap_var(k), wrap_var(item)) for k, item in v.items()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ".../ansible/utils/unsafe_proxy.py", line 350, in <genexpr> return dict((wrap_var(k), wrap_var(item)) for k, item in v.items()) ^^^^^^^^^^^^^^ File ".../ansible/utils/unsafe_proxy.py", line 380, in wrap_var v = AnsibleUnsafeText(v) ^^^^^^^^^^^^^^^^^^^^ File ".../ansible/utils/unsafe_proxy.py", line 213, in __new__ if s == 'rumplestiltskin': raise RuntimeError ^^^^^^^^^^^^^^^^^^ RuntimeError ...
Observations
-
ansible.utils.unsafe_proxy.*
is only available on the Ansible controller. The module isn't part ofansible.module_utils
, so not available on targets.
Informed guesses of rules/design principals
- The Ansible controller is the most valued security boundary, then targets.
- Ansible targets cannot/should not try to mark a value as safe or unsafe. The controller couldn't trust that determination anyway.