Skip to content

Commit 605b38d

Browse files
committed
fix: harden builder upload YAML validation
Reject fully-qualified (dotted) Python references in builder YAML uploads for name, agent_class, and code fields. Simple (non-dotted) names continue to work for ADK built-in tools and agent classes. Adds 6 tests covering rejection and allowlisting behavior.
1 parent d337ddf commit 605b38d

2 files changed

Lines changed: 106 additions & 1 deletion

File tree

src/google/adk/cli/fast_api.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,15 @@ def _has_parent_reference(path: str) -> bool:
307307
# Block any upload that contains an `args` key anywhere in the document.
308308
_BLOCKED_YAML_KEYS = frozenset({"args"})
309309

310+
# Fields that accept fully-qualified Python names and feed into
311+
# importlib.import_module() at agent load time. Dotted values in
312+
# these fields cause module-level code execution from any installed
313+
# package. Builder uploads must use simple (non-dotted) names that
314+
# resolve to ADK built-in types only.
315+
_IMPORT_REFERENCE_KEYS = frozenset({"name", "agent_class", "code"})
316+
310317
def _check_yaml_for_blocked_keys(content: bytes, filename: str) -> None:
311-
"""Raise if the YAML document contains any blocked keys."""
318+
"""Raise if the YAML document contains blocked keys or code refs."""
312319
import yaml
313320

314321
try:
@@ -325,6 +332,17 @@ def _walk(node: Any) -> None:
325332
f"The '{key}' field is not allowed in builder uploads "
326333
"because it can execute arbitrary code."
327334
)
335+
if (
336+
key in _IMPORT_REFERENCE_KEYS
337+
and isinstance(value, str)
338+
and "." in value
339+
):
340+
raise ValueError(
341+
f"Fully qualified Python reference in '{key}':"
342+
f" {value!r} in {filename!r} is not allowed in"
343+
" builder uploads. Only simple (non-dotted) names"
344+
" that resolve to ADK built-in types are permitted."
345+
)
328346
_walk(value)
329347
elif isinstance(node, list):
330348
for item in node:

tests/unittests/cli/test_fast_api.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2113,6 +2113,93 @@ def test_builder_save_rejects_nested_args_key(builder_test_client, tmp_path):
21132113
assert "args" in response.json()["detail"]
21142114

21152115

2116+
def test_builder_save_rejects_dotted_tool_name(builder_test_client, tmp_path):
2117+
"""Uploading YAML with a dotted tool name is rejected (import prevention)."""
2118+
yaml_with_dotted_tool = b"""\
2119+
name: my_agent
2120+
tools:
2121+
- name: os.system
2122+
"""
2123+
response = builder_test_client.post(
2124+
"/builder/save?tmp=true",
2125+
files=[(
2126+
"files",
2127+
("app/root_agent.yaml", yaml_with_dotted_tool, "application/x-yaml"),
2128+
)],
2129+
)
2130+
assert response.status_code == 400
2131+
assert "os.system" in response.json()["detail"]
2132+
assert not (tmp_path / "app" / "tmp" / "app" / "root_agent.yaml").exists()
2133+
2134+
2135+
def test_builder_save_rejects_dotted_agent_class(builder_test_client, tmp_path):
2136+
"""Uploading YAML with a dotted agent_class is rejected."""
2137+
yaml_with_dotted_class = b"""\
2138+
agent_class: evil.module.MyAgent
2139+
name: my_agent
2140+
"""
2141+
response = builder_test_client.post(
2142+
"/builder/save?tmp=true",
2143+
files=[(
2144+
"files",
2145+
("app/root_agent.yaml", yaml_with_dotted_class, "application/x-yaml"),
2146+
)],
2147+
)
2148+
assert response.status_code == 400
2149+
assert "evil.module.MyAgent" in response.json()["detail"]
2150+
2151+
2152+
def test_builder_save_rejects_dotted_code_ref(builder_test_client, tmp_path):
2153+
"""Uploading YAML with a dotted code reference is rejected."""
2154+
yaml_with_dotted_code = b"""\
2155+
name: my_agent
2156+
sub_agents:
2157+
- code: evil.module.my_agent
2158+
"""
2159+
response = builder_test_client.post(
2160+
"/builder/save?tmp=true",
2161+
files=[(
2162+
"files",
2163+
("app/root_agent.yaml", yaml_with_dotted_code, "application/x-yaml"),
2164+
)],
2165+
)
2166+
assert response.status_code == 400
2167+
assert "evil.module.my_agent" in response.json()["detail"]
2168+
2169+
2170+
def test_builder_save_allows_simple_tool_name(builder_test_client, tmp_path):
2171+
"""Uploading YAML with a simple (non-dotted) tool name is allowed."""
2172+
yaml_with_simple_tool = b"""\
2173+
name: my_agent
2174+
tools:
2175+
- name: google_search
2176+
"""
2177+
response = builder_test_client.post(
2178+
"/builder/save?tmp=true",
2179+
files=[(
2180+
"files",
2181+
("app/root_agent.yaml", yaml_with_simple_tool, "application/x-yaml"),
2182+
)],
2183+
)
2184+
assert response.status_code == 200
2185+
2186+
2187+
def test_builder_save_allows_simple_agent_class(builder_test_client, tmp_path):
2188+
"""Uploading YAML with a simple agent_class (e.g. LlmAgent) is allowed."""
2189+
yaml_with_simple_class = b"""\
2190+
agent_class: LlmAgent
2191+
name: my_agent
2192+
"""
2193+
response = builder_test_client.post(
2194+
"/builder/save?tmp=true",
2195+
files=[(
2196+
"files",
2197+
("app/root_agent.yaml", yaml_with_simple_class, "application/x-yaml"),
2198+
)],
2199+
)
2200+
assert response.status_code == 200
2201+
2202+
21162203
def test_builder_get_rejects_non_yaml_file_paths(builder_test_client, tmp_path):
21172204
"""GET /builder/app/{app_name}?file_path=... rejects non-YAML extensions."""
21182205
app_root = tmp_path / "app"

0 commit comments

Comments
 (0)