Skip to content

FIX Mitigate Jinja2 Server-Side Template Injection (SSTI) vulnerability#1577

Merged
romanlutz merged 6 commits intomicrosoft:mainfrom
romanlutz:fix/ssti-sandbox-environment
Apr 9, 2026
Merged

FIX Mitigate Jinja2 Server-Side Template Injection (SSTI) vulnerability#1577
romanlutz merged 6 commits intomicrosoft:mainfrom
romanlutz:fix/ssti-sandbox-environment

Conversation

@romanlutz
Copy link
Copy Markdown
Contributor

@romanlutz romanlutz commented Apr 7, 2026

Problem

PyRIT's template rendering in seed.py used an unsandboxed Jinja2 Environment, and 21 of 30 remote dataset loaders passed fetched data directly into SeedPrompt(value=...) without escaping. Since
SeedPrompt.__post_init__ renders self.value as a Jinja2 template, a poisoned remote dataset could achieve Python object traversal (e.g. "".__class__.__mro__[1].__subclasses__()) — confirmed via
proof-of-concept.

The 9 loaders that did wrap values in {% raw %}...{% endraw %} were also bypassable via {% endraw %} injection in the payload.

Changes

Layer 1 — Sandbox the rendering engine (seed.py)

  • Replace Environment() / Template() with SandboxedEnvironment from jinja2.sandbox
  • Blocks __class__, __mro__, __subclasses__() and other unsafe attribute access
  • All 40+ call sites across converters, scorers, and attack executors are covered since they all funnel through render_template_value / render_template_value_silent

Layer 2 — Safe-by-default SeedPrompt

  • Add is_jinja_template field to Seed (default False)
  • When False, __post_init__ auto-wraps the value in {% raw %}...{% endraw %} before rendering
  • Seed.from_yaml_file and SeedDataset.from_yaml_file set is_jinja_template=True for trusted local YAML templates
  • Dataset loaders no longer need explicit escaping — SeedPrompt(value=remote_data) is safe by default
  • Call sites that construct SeedPrompt with Jinja2 template syntax explicitly opt in with is_jinja_template=True

Layer 3 — Eliminate supply chain risk in many-shot jailbreak

  • Vendor the many-shot jailbreaking dataset (400 examples, ~664KB) locally as pyrit/datasets/jailbreak/many_shot_examples.json. The dataset was provided by @KutalVolkan originally. For simplicity, we're
    including it here as well.
  • Replace runtime requests.get() from GitHub URL with local json.load()
  • Rename fetch_many_shot_jailbreaking_datasetload_many_shot_jailbreaking_dataset
  • Deprecate fetch_many_shot_jailbreaking_dataset (removal in 0.14.0)

Regression tests

  • test_untrusted_seed_prompt_auto_escapes_template_syntax — verifies default auto-escaping
  • test_render_template_value_blocks_ssti_via_endraw_injection — verifies sandbox blocks {% endraw %} escape attack on trusted templates
  • test_render_template_value_silent_blocks_ssti_via_endraw_injection — same for the silent rendering path
  • test_seed_group_untrusted_auto_escapes — verifies SeedGroup propagates untrusted default

Testing

  • 2760+ unit tests pass (models, datasets, converters, executors, scorers, scenarios)
  • All pre-commit hooks pass (ruff, mypy, etc.)
  • Manual verification: SSTI payload returns SecurityError with sandbox, executes without
  • All 42 pliny jailbreak templates + many-shot template verified to render correctly

romanlutz and others added 5 commits April 7, 2026 12:37
Use SandboxedEnvironment instead of unsandboxed Environment/Template in
seed.py to block Python object traversal attacks (e.g. __class__.__mro__).

Add {% raw %}...{% endraw %} wrapping to 21 remote dataset loaders that
were passing fetched data directly into SeedPrompt/SeedObjective value
parameters without Jinja2 escaping. This prevents template injection
from poisoned remote datasets.

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

Co-authored-by: adrian-gavrila <50029937+adrian-gavrila@users.noreply.github.com>
Verify that SandboxedEnvironment blocks Python object traversal
when a malicious payload uses {% endraw %} to escape a {% raw %}
wrapper. Tests both render_template_value and render_template_value_silent.

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

Co-authored-by: adrian-gavrila <50029937+adrian-gavrila@users.noreply.github.com>
Replace remote fetch from GitHub URL with bundled JSON file to
eliminate supply chain risk. The dataset (400 examples from
KutalVolkan/many-shot-jailbreaking-dataset@5eac855) is now loaded
from pyrit/datasets/jailbreak/many_shot_examples.json.

Rename fetch_many_shot_jailbreaking_dataset to
load_many_shot_jailbreaking_dataset and remove requests dependency.

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

Co-authored-by: adrian-gavrila <50029937+adrian-gavrila@users.noreply.github.com>
Replace inline f-string raw wrapping across 31 remote dataset loaders
with a shared escape_jinja_template_syntax() function in
remote_dataset_loader.py. This makes the pattern discoverable and
harder to forget when adding new loaders.

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

Co-authored-by: adrian-gavrila <50029937+adrian-gavrila@users.noreply.github.com>
…ES_PATH


The vendored JSON file is at datasets/jailbreak/, not
datasets/jailbreak/templates/ — it's data, not a template.

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

Co-authored-by: adrian-gavrila <50029937+adrian-gavrila@users.noreply.github.com>
@romanlutz romanlutz requested a review from adrian-gavrila April 7, 2026 22:25
@romanlutz romanlutz force-pushed the fix/ssti-sandbox-environment branch from 47cad95 to 6b97a8b Compare April 7, 2026 22:27
Comment thread pyrit/datasets/seed_datasets/remote/toxic_chat_dataset.py
@romanlutz romanlutz force-pushed the fix/ssti-sandbox-environment branch 2 times, most recently from 58b23aa to ab55b31 Compare April 8, 2026 19:26
Comment thread pyrit/datasets/seed_datasets/remote/remote_dataset_loader.py Outdated
Comment thread pyrit/models/seeds/seed.py Outdated
Comment thread pyrit/executor/attack/single_turn/many_shot_jailbreak.py
@romanlutz romanlutz force-pushed the fix/ssti-sandbox-environment branch from ab55b31 to 3db7148 Compare April 9, 2026 17:35
Add jinja_template field to Seed (default False). When False,
__post_init__ automatically wraps the value in raw tags to prevent
template injection. Trusted sources (from_yaml_file) set
jinja_template=True to allow Jinja2 rendering.

This eliminates the need for callers to remember escape_jinja_template_syntax().
Dataset loaders no longer need explicit escaping — SeedPrompt(value=remote_data)
is safe by default.

Propagation: SeedGroup and SeedDataset from_yaml_file overrides pass
jinja_template=True through to nested seed construction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: adrian-gavrila <50029937+adrian-gavrila@users.noreply.github.com>
@romanlutz romanlutz force-pushed the fix/ssti-sandbox-environment branch from 3db7148 to befa47f Compare April 9, 2026 17:39
@romanlutz romanlutz merged commit 1f6cd32 into microsoft:main Apr 9, 2026
38 checks passed
@romanlutz romanlutz deleted the fix/ssti-sandbox-environment branch April 9, 2026 17:55
romanlutz added a commit that referenced this pull request Apr 9, 2026
…ty (#1577)

Co-authored-by: Roman Lutz <romanlutz@users.noreply.github.com>
Co-authored-by: adrian-gavrila <50029937+adrian-gavrila@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants