Skip to content

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

Open
romanlutz wants to merge 6 commits intomicrosoft:mainfrom
romanlutz:fix/ssti-sandbox-environment
Open

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

Conversation

@romanlutz
Copy link
Copy Markdown
Contributor

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 — Escape untrusted data in remote dataset loaders

  • Add escape_jinja_template_syntax() helper in remote_dataset_loader.py
  • Replace all 31 inline f"{{% raw %}}...{{% endraw %}}" patterns with the helper
  • Ensures future loaders have a discoverable, consistent function to call

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

Regression tests

  • test_render_template_value_blocks_ssti_via_endraw_injection — verifies sandbox blocks {% endraw %} escape attack
  • test_render_template_value_silent_blocks_ssti_via_endraw_injection — same for the silent rendering path

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

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
@romanlutz romanlutz force-pushed the fix/ssti-sandbox-environment branch from 9098c1c to 58b23aa Compare April 8, 2026 19:17
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 58b23aa to ab55b31 Compare April 8, 2026 19:26
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.

2 participants