Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/sagemaker/hyperpod/cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down
91 changes: 90 additions & 1 deletion test/unit_tests/cli/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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()
Loading