Skip to content

Commit a721c1e

Browse files
k4w-wakDeanChensj
authored andcommitted
fix(security): enable Jinja2 autoescape to prevent XSS in gepa sample
Merge google#5526 ## Security Fix: XSS via Jinja2 Template Injection (CWE-79) ### Vulnerability `contributing/samples/gepa/rater_lib.py` instantiates `jinja2.Environment()` **without** `autoescape=True`. The companion template `rubric_validation_template.txt` renders `{{user_input}}` and `{{model_response}}` without escaping. ### Impact Since ADK is Google's official framework for building AI agents, developers copy/adapt this sample code into production web applications. Unescaped user-controlled input in Jinja2 templates enables: - **Cross-Site Scripting (XSS)** — Arbitrary JavaScript execution in browsers - **Session Hijacking** — Steal cookies/tokens if rendered in web context - **Phishing** — Inject fake login forms ### Proof of Concept ```python # user_input: <script>alert("XSS")</script> # Renders as: <main_prompt><script>alert("XSS")</script></main_prompt> # model_response: <img src=x onerror=alert("XSS from model")> # Renders as: <responses><img src=x onerror=alert("XSS from model")></responses> ``` ### Changes 1. **rater_lib.py:170** — `jinja2.Environment()` → `jinja2.Environment(autoescape=True)` 2. **rubric_validation_template.txt:158** — `{{user_input}}` → `{{user_input|e}}` 3. **rubric_validation_template.txt:163** — `{{model_response}}` → `{{model_response|e}}` Defense in depth: `autoescape=True` provides baseline protection, explicit `|e` filters ensure escaping even if autoescape is later disabled. ### References - CWE-79: Cross-site Scripting (XSS) - OWASP A7:2017 — Cross-site Scripting - Jinja2 docs: https://jinja.palletsprojects.com/en/3.1.x/api/#autoescaping Co-authored-by: Shangjie Chen <deanchen@google.com> COPYBARA_INTEGRATE_REVIEW=google#5526 from k4w-wak:fix/jinja2-xss-autoescape b5b6d3e PiperOrigin-RevId: 943549514
1 parent 527e3c1 commit a721c1e

3 files changed

Lines changed: 82 additions & 3 deletions

File tree

contributing/samples/integrations/gepa/rater_lib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def __call__(self, messages: list[dict[str, Any]]) -> dict[str, Any]:
167167
Returns:
168168
A dictionary containing rating information including score.
169169
"""
170-
env = jinja2.Environment()
170+
env = jinja2.Environment(autoescape=True)
171171
env.globals['user_input'] = (
172172
messages[0].get('parts', [{}])[0].get('text', '') if messages else ''
173173
)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import sys
15+
from unittest import mock
16+
17+
# Mock the retry module before importing rater_lib
18+
mock_retry_module = mock.MagicMock()
19+
20+
21+
def mock_retry_decorator(*args, **kwargs):
22+
def decorator(func):
23+
return func
24+
25+
return decorator
26+
27+
28+
mock_retry_module.retry = mock_retry_decorator
29+
sys.modules["retry"] = mock_retry_module
30+
31+
from gepa import rater_lib
32+
33+
34+
def test_rater_escapes_html_inputs_to_prevent_xss():
35+
"""Rater escapes HTML tags in user and model inputs to prevent XSS.
36+
37+
Setup: Mock genai.Client to return a dummy rating response.
38+
Act: Call Rater with messages containing HTML tags.
39+
Assert: Verify that the rendered prompt template contains escaped HTML.
40+
"""
41+
# Arrange
42+
with mock.patch("google.genai.Client") as mock_client_cls:
43+
mock_client = mock_client_cls.return_value
44+
mock_generate = mock_client.models.generate_content
45+
mock_generate.return_value.text = (
46+
"Property: The agent fulfilled the user's primary request.\n"
47+
"Evidence: mock evidence\n"
48+
"Rationale: mock rationale\n"
49+
"Verdict: yes"
50+
)
51+
52+
template_path = (
53+
"contributing/samples/integrations/gepa/rubric_validation_template.txt"
54+
)
55+
rater = rater_lib.Rater(
56+
tool_declarations="[]", validation_template_path=template_path
57+
)
58+
59+
messages = [
60+
{
61+
"role": "user",
62+
"parts": [{"text": "<script>alert('XSS')</script>"}],
63+
},
64+
{
65+
"role": "model",
66+
"parts": [{"text": "Hello <img src=x onerror=alert(1)>"}],
67+
},
68+
]
69+
70+
# Act
71+
rater(messages)
72+
73+
# Assert
74+
assert mock_generate.called
75+
call_args = mock_generate.call_args
76+
contents = call_args.kwargs["contents"]
77+
78+
assert "&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;" in contents
79+
assert "Hello &lt;img src=x onerror=alert(1)&gt;" in contents

contributing/samples/integrations/gepa/rubric_validation_template.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,12 @@ Verdict: no
155155
</available_tools>
156156

157157
<main_prompt>
158-
{{user_input}}
158+
{{user_input|e}}
159159
</main_prompt>
160160
</user_prompt>
161161

162162
<responses>
163-
{{model_response}}
163+
{{model_response|e}}
164164
</responses>
165165

166166
<properties>

0 commit comments

Comments
 (0)