Skip to content

fix: render Jinja2 instructions in an immutable sandbox - #7212

Open
devjoinedthechat wants to merge 1 commit into
google:mainfrom
devjoinedthechat:fix/jinja2-immutable-sandbox
Open

devjoinedthechat wants to merge 1 commit into
google:mainfrom
devjoinedthechat:fix/jinja2-immutable-sandbox

Conversation

@devjoinedthechat

Copy link
Copy Markdown

Link to Issue or Description of Change

Problem:

inject_session_state(..., use_jinja2=True) renders with a plain jinja2.Environment, so a template can reach Python internals, e.g. ''.__class__.__mro__[1].__subclasses__() or the artifact helper's __globals__.

Separately, the template context is dict(session.state), a shallow copy, so a template can also mutate the live state objects in place ({{ items.append('b') }}, {{ user.update(name='Bar') }}). That change never goes through a state delta.

Solution:

Render with jinja2.sandbox.ImmutableSandboxedEnvironment. It is the SandboxedEnvironment already used for the eval prompt templates, plus blocking the mutating methods of builtin lists, dicts and sets, which fits the read-only context this function gets. Variables, filters, conditionals, loops and the async artifact() helper behave as before.

If you'd prefer plain SandboxedEnvironment to match the eval code, it's a one-word change.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

New tests: test_inject_session_state_jinja2_blocks_python_internals (2 cases) and test_inject_session_state_jinja2_cannot_mutate_state. All three fail on main.

tests/unittests/flows/llm_flows/prompt/test_instructions_utils.py  32 passed  (Python 3.10, 3.11, 3.14)
tests/unittests/flows/llm_flows                                    844 passed (Python 3.11)

Manual End-to-End (E2E) Tests:

Script using a real InMemorySessionService session and ReadonlyContext:

session = await service.create_session(
    app_name="app", user_id="u", state={"items": ["a"], "user": {"name": "Foo"}}
)
ctx = ReadonlyContext(InvocationContext(session_service=service, invocation_id="inv",
    agent=Agent(name="agent", model="gemini-2.5-flash"), session=session))
for template in (
    "{{ ''.__class__.__mro__[1].__subclasses__() | length }} classes",
    "{{ items.append('b') }}{{ user.update(name='Bar') }}",
    "Hi {{ user.name }}{% for i in items %} [{{ i }}]{% endfor %}",
):
  print(await inject_session_state(template, ctx, use_jinja2=True))
print(session.state)

Before (main):

"{{ ''.__class__.__mro__[1].__subclasses__() | length }} classes" -> '983 classes'
"{{ items.append('b') }}{{ user.update(name='Bar') }}" -> 'NoneNone'
'Hi {{ user.name }}{% for i in items %} [{{ i }}]{% endfor %}' -> 'Hi Bar [a] [b]'
session.state: {'items': ['a', 'b'], 'user': {'name': 'Bar'}}

After:

"{{ ''.__class__.__mro__[1].__subclasses__() | length }} classes" -> SecurityError: access to attribute '__class__' of 'str' object is unsafe.
"{{ items.append('b') }}{{ user.update(name='Bar') }}" -> SecurityError: access to attribute 'append' of 'list' object is unsafe.
'Hi {{ user.name }}{% for i in items %} [{{ i }}]{% endfor %}' -> 'Hi Foo [a]'
session.state: {'items': ['a'], 'user': {'name': 'Foo'}}

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules. (none)

use_jinja2 rendered templates with a plain jinja2.Environment, so a
template could reach Python internals through attributes such as
__class__ and __globals__. The template context also holds the live
session state objects, so a template could modify them in place.

Render with ImmutableSandboxedEnvironment, which blocks both. Follow-up
to google#6186.
@google-cla

google-cla Bot commented Sep 19, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

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