From 88ef335267393786d8db68fcde38a723983a280b Mon Sep 17 00:00:00 2001 From: Mufaddal Rohawala Date: Wed, 21 Oct 2026 12:14:28 -0700 Subject: [PATCH] fix: sandbox Jinja2 rendering in standard template create path `hyp create` rendered standard project template manifests (k8s.jinja / cfn_params.jinja) with a plain `jinja2.Template`. Rendering now uses `jinja2.sandbox.ImmutableSandboxedEnvironment` so template expressions cannot access unsafe Python attributes or callables. The recipe rendering path (recipe_utils._render_k8s_template) already used a sandboxed environment; this aligns the standard-template path in _create_standard_template with it. Adds tests asserting that a disallowed template expression is blocked and that benign templates still render. --- src/sagemaker/hyperpod/cli/commands/init.py | 9 +- test/unit_tests/cli/test_init.py | 91 ++++++++++++++++++++- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/src/sagemaker/hyperpod/cli/commands/init.py b/src/sagemaker/hyperpod/cli/commands/init.py index 8cfb15c1..39bac4ba 100644 --- a/src/sagemaker/hyperpod/cli/commands/init.py +++ b/src/sagemaker/hyperpod/cli/commands/init.py @@ -4,7 +4,7 @@ import shutil from pathlib import Path from datetime import datetime -from jinja2 import Template +from jinja2.sandbox import ImmutableSandboxedEnvironment from sagemaker.hyperpod.cli.constants.init_constants import ( USAGE_GUIDE_TEXT_CFN, USAGE_GUIDE_TEXT_CRD, @@ -435,7 +435,12 @@ def _create_standard_template(dir_path: Path, data: dict, template: str, version try: template_source = jinja_file.read_text() - tpl = Template(template_source) + # Render template manifests inside a sandboxed Jinja2 environment so + # that template expressions cannot access unsafe Python attributes or + # callables. This matches the sandboxed rendering used elsewhere + # (recipe_utils._render_k8s_template) and keeps the two paths aligned. + env = ImmutableSandboxedEnvironment() + tpl = env.from_string(template_source) rendered = tpl.render(**data) except Exception as e: click.secho(f"❌ Failed to render template: {e}", fg="red") diff --git a/test/unit_tests/cli/test_init.py b/test/unit_tests/cli/test_init.py index dbfb5c6b..9df1853d 100644 --- a/test/unit_tests/cli/test_init.py +++ b/test/unit_tests/cli/test_init.py @@ -20,7 +20,7 @@ } } }) - from sagemaker.hyperpod.cli.commands.init import init, reset, configure, validate, _default_create + from sagemaker.hyperpod.cli.commands.init import init, reset, configure, validate, _default_create, _create_standard_template from sagemaker.hyperpod.cli.constants.init_constants import CFN, CRD @@ -1161,3 +1161,92 @@ def test_invalid_field_validation(self): assert 'environment' not in custom_inference_fields assert 'env' not in pytorch_fields + + +class TestTemplateRenderingSandbox: + """Tests that `hyp create` renders template manifests inside a sandbox. + + Template manifests (`k8s.jinja` / `cfn_params.jinja`) come from the project + scaffold on disk. Rendering them must use a sandboxed Jinja2 environment so + that template expressions cannot reach unsafe Python attributes or + callables, consistent with the recipe rendering path. These tests assert the + sandbox is active and that benign templates still render. + """ + + # A benign expression that calls a mutating method on a literal. An + # ImmutableSandboxedEnvironment rejects this; a plain jinja2.Template would + # allow the method call. It performs no I/O. + SANDBOX_TRIP_EXPR = "{{ [].append(1) }}" + + def _run_create(self, tmp_path, jinja_body): + """Write a k8s.jinja + config.yaml and invoke the standard template + create path. Returns the raised exception (or None).""" + dir_path = Path(tmp_path) + (dir_path / "config.yaml").write_text("test: value\n") + (dir_path / "k8s.jinja").write_text(jinja_body) + + templates_stub = { + "hyp-pytorch-job": {"schema_type": CRD, "registry": {}} + } + with patch( + "sagemaker.hyperpod.cli.commands.init.TEMPLATES", templates_stub + ): + try: + _create_standard_template( + dir_path=dir_path, + data={"test": "value"}, + template="hyp-pytorch-job", + version="1.0", + region=None, + template_version=1, + ) + except SystemExit as e: + return e + except Exception as e: # noqa: BLE001 + return e + return None + + def test_disallowed_expression_is_blocked(self, tmp_path): + """A template expression that the sandbox disallows must cause render + to exit with an error rather than evaluating the expression.""" + result = self._run_create(tmp_path, self.SANDBOX_TRIP_EXPR) + + # _create_standard_template calls sys.exit(1) when render raises. + assert isinstance(result, SystemExit) + assert result.code == 1 + + # No rendered output should have been written for the rejected template. + run_dir = Path(tmp_path) / "run" + rendered = list(run_dir.glob("*/k8s.yaml")) if run_dir.exists() else [] + assert not rendered, "Disallowed expression was rendered instead of blocked" + + def test_legitimate_template_still_renders(self, tmp_path): + """A benign template with normal variable substitution still works.""" + dir_path = Path(tmp_path) + (dir_path / "config.yaml").write_text("job_name: my-job\n") + (dir_path / "k8s.jinja").write_text("name: {{ job_name }}\n") + + templates_stub = { + "hyp-pytorch-job": {"schema_type": CRD, "registry": {}} + } + with patch( + "sagemaker.hyperpod.cli.commands.init.TEMPLATES", templates_stub + ): + try: + _create_standard_template( + dir_path=dir_path, + data={"job_name": "my-job"}, + template="hyp-pytorch-job", + version="1.0", + region=None, + template_version=1, + ) + except SystemExit: + pass # Downstream K8s/AWS calls may fail in a unit environment. + except Exception: + pass + + run_dir = dir_path / "run" + rendered_files = list(run_dir.glob("*/k8s.yaml")) if run_dir.exists() else [] + assert rendered_files, "Benign template failed to render" + assert "name: my-job" in rendered_files[0].read_text()