Skip to content

Use ImmutableSandboxedEnvironment in jinja.py to prevent SSTI#1346

Open
JAE0Y2N wants to merge 1 commit into
PrefectHQ:mainfrom
JAE0Y2N:fix-jinja-ssti-sandbox
Open

Use ImmutableSandboxedEnvironment in jinja.py to prevent SSTI#1346
JAE0Y2N wants to merge 1 commit into
PrefectHQ:mainfrom
JAE0Y2N:fix-jinja-ssti-sandbox

Conversation

@JAE0Y2N

@JAE0Y2N JAE0Y2N commented May 19, 2026

Copy link
Copy Markdown

The Jinja env in src/marvin/utilities/jinja.py was a raw Environment with inspect and os.getcwd exposed as globals. Any string that reaches Template.render() from external input — prompt_template on Task / Agent / Memory / Team / Actor — could escape to arbitrary Python via either the explicit globals ({{ inspect.getmembers(os) }}) or the implicit class-traversal vectors ({{ ''.__class__.__mro__[1].__subclasses__() }} and friends).

Fix is three small changes in one file:

  1. Swap Environment for jinja2.sandbox.ImmutableSandboxedEnvironment. That blocks the standard class-traversal chain (__class__ / __init__ / __globals__ / __subclasses__) at the sandbox layer.
  2. Drop inspect from global_fns. Not referenced by any shipped template — grepped src/marvin/templates/ — so removing it doesn't break anything and removes the most direct escape primitive.
  3. Drop getcwd from global_fns. Same reasoning — not used, and exposes host cwd to template consumers for no clear benefit.

Verified all 5 shipped templates (task, memory, system, agent, team) load and parse under the sandboxed env. The sandbox raises SecurityError on disallowed attribute access, which is the right behavior — non-malicious templates that only read Marvin's typed model attributes (task.id, agent.name, etc.) keep working unchanged.

Background: I emailed this to bugbounty@prefect.io on May 17. The team confirmed the finding is technically valid but PrefectHQ/marvin is out of bounty scope (only Prefect Cloud / Prefect OSS / Horizon are in scope), and invited a PR — this is that PR.

The previous JinjaEnvironment exposed `inspect` and `os.getcwd` as Jinja
globals and used the raw (non-sandboxed) Environment. Any string that
flows into `Template.render()` from external input could escape via
either explicit globals (`{{ inspect.getmembers(os) }}`) or Python's
implicit class-traversal vectors (`{{ ''.__class__.__mro__[1].__subclasses__() }}`).

Marvin exposes prompt_template as a public field on Task, Agent, Memory,
Team, and Actor. If any of those flow from external input (LLM-generated,
HTTP request, file load), the SSTI primitive reaches arbitrary Python
execution in the host process.

Three changes:

  1. Replace `from jinja2 import Environment as JinjaEnvironment` with
     `from jinja2.sandbox import ImmutableSandboxedEnvironment as JinjaEnvironment`.
     This blocks `__class__`, `__init__`, `__globals__`, `__builtins__`,
     `__import__`, `__subclasses__`, and the rest of the standard
     class-traversal escape chain.

  2. Remove `inspect` from global_fns. It was not referenced by any
     shipped template (grepped src/marvin/templates/) and its only
     plausible runtime use is the SSTI escape path itself.

  3. Remove `getcwd` from global_fns. Same reasoning: not used by any
     template, and exposes the host's working directory to template
     consumers.

Verified that all 5 shipped templates (task, memory, system, agent,
team) load + parse cleanly under ImmutableSandboxedEnvironment. The
SecurityError raised by the sandbox is the expected behavior on
disallowed attribute access; non-malicious templates that only access
Marvin's own typed model attributes (task.id, agent.name, etc.) continue
to render normally.

Disclosed to bugbounty@prefect.io 2026-05-17; out of bounty scope per
the program (only Prefect Cloud / Prefect OSS / Horizon are in scope),
but the team explicitly invited a PR.
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.

1 participant