From 3554f71a76355882e8b7176721646e987d4b1b27 Mon Sep 17 00:00:00 2001 From: Tyler Coatsworth <14064505+tcoatswo@users.noreply.github.com> Date: Tue, 19 May 2026 05:39:47 +0000 Subject: [PATCH] Harden Jinja template rendering with sandboxing --- cloudinit/handlers/jinja_template.py | 4 +++- cloudinit/templater.py | 14 +++++++------- tests/unittests/test_builtin_handlers.py | 20 ++++++++++++++++++++ tests/unittests/test_templating.py | 11 +++++++++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/cloudinit/handlers/jinja_template.py b/cloudinit/handlers/jinja_template.py index 388588d8029..97b2d4fc752 100644 --- a/cloudinit/handlers/jinja_template.py +++ b/cloudinit/handlers/jinja_template.py @@ -22,10 +22,12 @@ JUndefinedError: Type[Exception] try: from jinja2.exceptions import UndefinedError as JUndefinedError + from jinja2.exceptions import SecurityError as JSecurityError from jinja2.lexer import operator_re except ImportError: # No jinja2 dependency JUndefinedError = Exception + JSecurityError = Exception operator_re = re.compile(r"[-.]") LOG = logging.getLogger(__name__) @@ -147,7 +149,7 @@ def render_jinja_payload(payload, payload_fn, instance_data, debug=False): ) try: rendered_payload = render_string(payload, instance_jinja_vars) - except (TypeError, JUndefinedError) as e: + except (TypeError, JUndefinedError, JSecurityError) as e: LOG.warning("Ignoring jinja template for %s: %s", payload_fn, str(e)) return None warnings = [ diff --git a/cloudinit/templater.py b/cloudinit/templater.py index b33f0c95a2e..ce90a8fad71 100644 --- a/cloudinit/templater.py +++ b/cloudinit/templater.py @@ -31,7 +31,7 @@ JUndefined: Any try: from jinja2 import DebugUndefined as _DebugUndefined - from jinja2 import Template as JTemplate + from jinja2.sandbox import SandboxedEnvironment as JSandboxedEnvironment JINJA_AVAILABLE = True JUndefined = _DebugUndefined @@ -149,13 +149,13 @@ def jinja_render(content, params): add = "\n" if content.endswith("\n") else "" try: with performance.Timed("Rendering jinja2 template"): + jinja_env = JSandboxedEnvironment( + undefined=UndefinedJinjaVariable, + trim_blocks=True, + extensions=["jinja2.ext.do"], + ) return ( - JTemplate( - content, - undefined=UndefinedJinjaVariable, - trim_blocks=True, - extensions=["jinja2.ext.do"], - ).render(**params) + jinja_env.from_string(content).render(**params) + add ) except TemplateSyntaxError as template_syntax_error: diff --git a/tests/unittests/test_builtin_handlers.py b/tests/unittests/test_builtin_handlers.py index 14edb44cde2..d02568dbb8d 100644 --- a/tests/unittests/test_builtin_handlers.py +++ b/tests/unittests/test_builtin_handlers.py @@ -393,6 +393,26 @@ def test_render_jinja_payload_replaces_missing_variables_and_warns( ) assert expected_log in caplog.text + @skipUnlessJinja() + def test_render_jinja_payload_blocks_unsafe_attribute_access( + self, caplog + ): + payload = ( + "## template: jinja\n" + "{{ ''.__class__.__mro__[1].__subclasses__()[:3] }}" + ) + + assert ( + render_jinja_payload( + payload=payload, + payload_fn="myfile", + instance_data={"v1": {"hostname": "foo"}}, + ) + is None + ) + assert "Ignoring jinja template for myfile" in caplog.text + assert "__class__" in caplog.text + class TestShellScriptByFrequencyHandlers: @pytest.fixture(autouse=True) diff --git a/tests/unittests/test_templating.py b/tests/unittests/test_templating.py index 83c1ba4fc69..b2caa92a2b3 100644 --- a/tests/unittests/test_templating.py +++ b/tests/unittests/test_templating.py @@ -9,6 +9,7 @@ from unittest import mock import pytest +from jinja2.exceptions import SecurityError from cloudinit import templater from cloudinit.templater import JinjaSyntaxParsingException @@ -171,6 +172,16 @@ def test_jinja_do_extension_render_to_string(self): == expected_result ) + @test_helpers.skipUnlessJinja() + def test_jinja_blocks_unsafe_attribute_access(self): + template = self.add_header( + "jinja", + "{{ ''.__class__.__mro__[1].__subclasses__()[:3] }}", + ) + + with pytest.raises(SecurityError): + templater.render_string(template, {}) + class TestJinjaSyntaxParsingException: def test_jinja_syntax_parsing_exception_message(self):