Skip to content

Use Jinja2 SandboxedEnvironment to prevent SSTI/RCE in azure-ai-evaluation#46182

Open
w-javed wants to merge 2 commits intoAzure:mainfrom
w-javed:fix/jinja-sandbox-ssti
Open

Use Jinja2 SandboxedEnvironment to prevent SSTI/RCE in azure-ai-evaluation#46182
w-javed wants to merge 2 commits intoAzure:mainfrom
w-javed:fix/jinja-sandbox-ssti

Conversation

@w-javed
Copy link
Copy Markdown
Contributor

@w-javed w-javed commented Apr 7, 2026

Problem

ICM: 31000000565443 (MSRC-110257)

Unsandboxed Jinja2 template rendering in azure-ai-evaluation allows Server-Side Template Injection (SSTI) that can escalate to arbitrary Remote Code Execution (RCE). Two files use jinja2.Template() directly without sandboxing:

  • _legacy/prompty/_utils.pyrender_jinja_template()
  • simulator/_conversation/__init__.pyConversationBot template creation (2 call sites)

An attacker can exploit this via crafted template strings containing __class__.__base__.__subclasses__() chains to execute arbitrary OS commands.

Fix

Replace raw jinja2.Template() / jinja2.Environment with jinja2.sandbox.SandboxedEnvironment, matching the existing pattern in PromptFlow (promptflow-core and promptflow-tools).

_legacy/prompty/_utils.py

render_jinja_template() now checks PF_USE_SANDBOX_FOR_JINJA env var (defaults to "true") and uses SandboxedEnvironment.from_string() when enabled.

simulator/_conversation/__init__.py

Added _create_jinja_template() helper that conditionally creates templates via SandboxedEnvironment. Replaced both jinja2.Template() call sites:

  • ConversationBot.__init__() — conversation template
  • ConversationBot.__init__() — conversation starter template

Behavior

PF_USE_SANDBOX_FOR_JINJA Behavior
true (default) SandboxedEnvironment blocks unsafe attribute access (__class__, __subclasses__, etc.)
false Falls back to standard Template (opt-out, not recommended)

Tests

  • 67 existing tests pass (simulator, conversation, prompty, jinja-related)
  • Manually verified SSTI payload {{ ().__class__.__base__.__subclasses__() }} is blocked with SecurityError
  • Normal template rendering works unchanged

…ation

Add SandboxedEnvironment from jinja2.sandbox to both vulnerable files
identified in MSRC-110257:
- _legacy/prompty/_utils.py: render_jinja_template()
- simulator/_conversation/__init__.py: ConversationBot template creation

Sandbox is enabled by default (matching PromptFlow behavior). Set
PF_USE_SANDBOX_FOR_JINJA=false to opt out if needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@w-javed w-javed requested a review from a team as a code owner April 7, 2026 15:35
@w-javed w-javed added the Evaluation Issues related to the client library for Azure AI Evaluation label Apr 7, 2026
Copilot AI review requested due to automatic review settings April 7, 2026 15:35
19 tests covering:
- render_jinja_template sandbox (SSTI blocked, normal renders, opt-out)
- _create_jinja_template sandbox (SSTI blocked, StrictUndefined preserved)
- ConversationBot integration (template + starter both sandboxed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR mitigates Jinja2 Server-Side Template Injection (SSTI) / potential RCE in azure-ai-evaluation by switching template creation to Jinja2’s SandboxedEnvironment by default (with an env-var opt-out for compatibility).

Changes:

  • Added a sandboxed Jinja2 template factory in the simulator conversation module and replaced direct jinja2.Template(...) usage.
  • Updated legacy prompty Jinja rendering to use SandboxedEnvironment.from_string() by default, gated by PF_USE_SANDBOX_FOR_JINJA.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/simulator/_conversation/__init__.py Introduces _create_jinja_template() using SandboxedEnvironment by default and routes conversation/starter templates through it.
sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/_legacy/prompty/_utils.py Updates render_jinja_template() to use SandboxedEnvironment by default with an env-var opt-out.

Comment on lines +25 to +34
def _create_jinja_template(template_content: str) -> jinja2.Template:
"""Create a Jinja2 template, using SandboxedEnvironment by default to prevent SSTI attacks.

Set env var PF_USE_SANDBOX_FOR_JINJA=false to opt out (not recommended).
"""
use_sandbox = os.environ.get("PF_USE_SANDBOX_FOR_JINJA", "true")
if use_sandbox.lower() == "false":
return jinja2.Template(template_content, undefined=jinja2.StrictUndefined)
sandbox_env = SandboxedEnvironment(undefined=jinja2.StrictUndefined)
return sandbox_env.from_string(template_content)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a focused regression test for the sandboxed Jinja path (default) to ensure a known SSTI payload (e.g., accessing class/subclasses) raises jinja2.exceptions.SecurityError, and that PF_USE_SANDBOX_FOR_JINJA=false opts out as expected. This helps prevent accidental reintroduction of unsandboxed rendering in the simulator conversation templates.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +31
def _create_jinja_template(template_content: str) -> jinja2.Template:
"""Create a Jinja2 template, using SandboxedEnvironment by default to prevent SSTI attacks.

Set env var PF_USE_SANDBOX_FOR_JINJA=false to opt out (not recommended).
"""
use_sandbox = os.environ.get("PF_USE_SANDBOX_FOR_JINJA", "true")
if use_sandbox.lower() == "false":
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PF_USE_SANDBOX_FOR_JINJA parsing/behavior toggle is now duplicated here and in _legacy/prompty/_utils.py. To reduce the chance of the two implementations drifting (default value, accepted falsey values, StrictUndefined config), consider centralizing this into a shared helper or constant in a common module.

Suggested change
def _create_jinja_template(template_content: str) -> jinja2.Template:
"""Create a Jinja2 template, using SandboxedEnvironment by default to prevent SSTI attacks.
Set env var PF_USE_SANDBOX_FOR_JINJA=false to opt out (not recommended).
"""
use_sandbox = os.environ.get("PF_USE_SANDBOX_FOR_JINJA", "true")
if use_sandbox.lower() == "false":
_PF_USE_SANDBOX_FOR_JINJA_ENV_VAR = "PF_USE_SANDBOX_FOR_JINJA"
_PF_USE_SANDBOX_FOR_JINJA_DEFAULT = "true"
_PF_USE_SANDBOX_FOR_JINJA_FALSEY_VALUES = {"false"}
def _use_sandbox_for_jinja() -> bool:
"""Return whether sandboxed Jinja rendering is enabled."""
use_sandbox = os.environ.get(_PF_USE_SANDBOX_FOR_JINJA_ENV_VAR, _PF_USE_SANDBOX_FOR_JINJA_DEFAULT)
return use_sandbox.lower() not in _PF_USE_SANDBOX_FOR_JINJA_FALSEY_VALUES
def _create_jinja_template(template_content: str) -> jinja2.Template:
"""Create a Jinja2 template, using SandboxedEnvironment by default to prevent SSTI attacks.
Set env var PF_USE_SANDBOX_FOR_JINJA=false to opt out (not recommended).
"""
if not _use_sandbox_for_jinja():

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +257
sanitized_template = sandbox_env.from_string(template_str)
return sanitized_template.render(**kwargs)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name "sanitized_template" is misleading here: SandboxedEnvironment.from_string() returns a compiled template but does not sanitize the template content. Renaming to something like "sandboxed_template"/"template" would better reflect what the value represents and avoid confusion when reading security-related code.

Suggested change
sanitized_template = sandbox_env.from_string(template_str)
return sanitized_template.render(**kwargs)
sandboxed_template = sandbox_env.from_string(template_str)
return sandboxed_template.render(**kwargs)

Copilot uses AI. Check for mistakes.
Comment on lines 246 to +257
def render_jinja_template(template_str: str, *, trim_blocks=True, keep_trailing_newline=True, **kwargs) -> str:
try:
template = Template(template_str, trim_blocks=trim_blocks, keep_trailing_newline=keep_trailing_newline)
return template.render(**kwargs)
use_sandbox_env = os.environ.get("PF_USE_SANDBOX_FOR_JINJA", "true")
if use_sandbox_env.lower() == "false":
template = Template(template_str, trim_blocks=trim_blocks, keep_trailing_newline=keep_trailing_newline)
return template.render(**kwargs)
else:
sandbox_env = SandboxedEnvironment(
trim_blocks=trim_blocks, keep_trailing_newline=keep_trailing_newline
)
sanitized_template = sandbox_env.from_string(template_str)
return sanitized_template.render(**kwargs)
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a unit/regression test that verifies render_jinja_template uses SandboxedEnvironment by default and blocks a representative SSTI payload with jinja2.exceptions.SecurityError. Given this is a security fix, a test would help catch future regressions (including the PF_USE_SANDBOX_FOR_JINJA opt-out behavior).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Evaluation Issues related to the client library for Azure AI Evaluation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants