From 5fd78b07fb6b4661f456a212c5720194c6390cda Mon Sep 17 00:00:00 2001 From: Matt Davis <6775756+nitzmahone@users.noreply.github.com> Date: Thu, 29 May 2025 18:37:58 -0700 Subject: [PATCH] Added _TEMPLAR_SANDBOX_MODE config (#85222) * Added _TEMPLAR_SANDBOX_MODE config * allows unsafe attribute checks to be disabled in Jinja sandbox * Update lib/ansible/_internal/_templating/_jinja_bits.py Co-authored-by: Matt Clay --------- Co-authored-by: Matt Clay (cherry picked from commit 91453e30afb2bef252049e73d853be06e7af8598) --- changelogs/fragments/sandbox_config.yml | 2 ++ .../_internal/_templating/_jinja_bits.py | 8 ++++++++ .../_internal/_templating/_jinja_common.py | 7 +++++++ lib/ansible/config/base.yml | 13 +++++++++++++ .../units/_internal/templating/test_templar.py | 18 +++++++++++++++++- 5 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/sandbox_config.yml diff --git a/changelogs/fragments/sandbox_config.yml b/changelogs/fragments/sandbox_config.yml new file mode 100644 index 00000000000..0f17016dc97 --- /dev/null +++ b/changelogs/fragments/sandbox_config.yml @@ -0,0 +1,2 @@ +minor_changes: + - templating - Added ``_ANSIBLE_TEMPLAR_SANDBOX_MODE=allow_unsafe_attributes`` environment variable to disable Jinja template attribute sandbox. (https://github.com/ansible/ansible/issues/85202) diff --git a/lib/ansible/_internal/_templating/_jinja_bits.py b/lib/ansible/_internal/_templating/_jinja_bits.py index 8603307c4e5..f8ed4712049 100644 --- a/lib/ansible/_internal/_templating/_jinja_bits.py +++ b/lib/ansible/_internal/_templating/_jinja_bits.py @@ -49,6 +49,7 @@ from ._jinja_common import ( TruncationMarker, validate_arg_type, JinjaCallContext, + _SandboxMode, ) from ._jinja_plugins import JinjaPluginIntercept, _query, _lookup, _now, _wrap_plugin_output, get_first_marker_arg, _DirectCall, _jinja_const_template_warning from ._lazy_containers import ( @@ -588,6 +589,13 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment): return template_obj + def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool: + # deprecated: description="remove relaxed template sandbox mode support" core_version="2.23" + if _TemplateConfig.sandbox_mode == _SandboxMode.ALLOW_UNSAFE_ATTRIBUTES: + return True + + return super().is_safe_attribute(obj, attr, value) + @property def lexer(self) -> AnsibleLexer: """Return/cache an AnsibleLexer with settings from the current AnsibleEnvironment""" diff --git a/lib/ansible/_internal/_templating/_jinja_common.py b/lib/ansible/_internal/_templating/_jinja_common.py index 42413f951b1..3d361dce0e8 100644 --- a/lib/ansible/_internal/_templating/_jinja_common.py +++ b/lib/ansible/_internal/_templating/_jinja_common.py @@ -2,6 +2,7 @@ from __future__ import annotations import abc import collections.abc as c +import enum import inspect import itertools import typing as t @@ -24,10 +25,16 @@ from ...module_utils.datatag import native_type_name _patch_jinja() # apply Jinja2 patches before types are declared that are dependent on the changes +class _SandboxMode(enum.Enum): + DEFAULT = enum.auto() + ALLOW_UNSAFE_ATTRIBUTES = enum.auto() + + class _TemplateConfig: allow_embedded_templates: bool = config.get_config_value("ALLOW_EMBEDDED_TEMPLATES") allow_broken_conditionals: bool = config.get_config_value('ALLOW_BROKEN_CONDITIONALS') jinja_extensions: list[str] = config.get_config_value('DEFAULT_JINJA2_EXTENSIONS') + sandbox_mode: _SandboxMode = _SandboxMode.__members__[config.get_config_value('_TEMPLAR_SANDBOX_MODE').upper()] unknown_type_encountered_handler = ErrorHandler.from_config('_TEMPLAR_UNKNOWN_TYPE_ENCOUNTERED') unknown_type_conversion_handler = ErrorHandler.from_config('_TEMPLAR_UNKNOWN_TYPE_CONVERSION') diff --git a/lib/ansible/config/base.yml b/lib/ansible/config/base.yml index 72905dcf2f2..7cbb2fd26e9 100644 --- a/lib/ansible/config/base.yml +++ b/lib/ansible/config/base.yml @@ -2036,6 +2036,19 @@ TASK_TIMEOUT: - {key: task_timeout, section: defaults} type: integer version_added: '2.10' +_TEMPLAR_SANDBOX_MODE: + name: Control Jinja template sandbox behavior + default: default + description: + - The default Jinja sandbox behavior blocks template access to all `_` prefixed object attributes and known collection mutation methods (e.g., `dict.clear()`, `list.append()`). + type: choices + choices: + - default + - allow_unsafe_attributes + env: [{name: _ANSIBLE_TEMPLAR_SANDBOX_MODE}] + deprecated: + why: controlling sandbox behavior is a temporary workaround + version: '2.23' _TEMPLAR_UNKNOWN_TYPE_CONVERSION: name: Templar unknown type conversion behavior default: warning diff --git a/test/units/_internal/templating/test_templar.py b/test/units/_internal/templating/test_templar.py index 3a6bb2b1ac8..fc15953099e 100644 --- a/test/units/_internal/templating/test_templar.py +++ b/test/units/_internal/templating/test_templar.py @@ -43,7 +43,7 @@ from ansible._internal._templating import _transform from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder from ansible._internal._datatag._tags import Origin, TrustedAsTemplate from ansible.plugins.loader import init_plugin_loader -from ansible._internal._templating._jinja_common import _TemplateConfig +from ansible._internal._templating._jinja_common import _TemplateConfig, _SandboxMode from ansible._internal._templating._jinja_plugins import _lookup from ansible._internal._templating import _jinja_plugins from ansible._internal._templating._engine import TemplateEngine, TemplateOptions @@ -1058,3 +1058,19 @@ def test_jinja_const_template_finalized() -> None: with _DeferredWarningContext(variables={}): # suppress warning from usage of embedded template with unittest.mock.patch.object(_TemplateConfig, 'allow_embedded_templates', True): assert not _JinjaConstTemplate.is_tagged_on(TemplateEngine().template(TRUST.tag("{{ '{{ 1 }}' }}"))) + + +@pytest.mark.parametrize("template,expected", ( + ("{% set x=[] %}{% set _=x.append(42) %}{{ x }}", [42]), + ("{{ (32).__or__(64) }}", 96), + ("{% set x={'foo': 42} %}{% set _=x.clear() %}{{ x }}", {}), +)) +def test_unsafe_attr_access(template: str, expected: object) -> None: + """Verify that unsafe attribute access fails by default and works when explicitly configured.""" + assert _TemplateConfig.sandbox_mode == _SandboxMode.DEFAULT + + with pytest.raises(AnsibleUndefinedVariable): + TemplateEngine().template(TRUST.tag(template)) + + with unittest.mock.patch.object(_TemplateConfig, 'sandbox_mode', _SandboxMode.ALLOW_UNSAFE_ATTRIBUTES): + assert TemplateEngine().template(TRUST.tag(template)) == expected