Skip to content

Commit 116f75d

Browse files
sasha-gitgcopybara-github
authored andcommitted
fix: add agent name validation to prevent arbitrary module imports
Co-authored-by: Sasha Sobran <asobran@google.com> PiperOrigin-RevId: 889890969
1 parent d112131 commit 116f75d

2 files changed

Lines changed: 84 additions & 42 deletions

File tree

‎src/google/adk/cli/utils/agent_loader.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
importlogging
2020
importos
2121
frompathlibimportPath
22+
importre
2223
importsys
2324
fromtypingimportAny
2425
fromtypingimportLiteral
@@ -187,8 +188,36 @@ def _load_from_yaml_config(
187188
) +e.args[1:]
188189
raisee
189190

191+
_VALID_AGENT_NAME_RE=re.compile(r"^[a-zA-Z0-9_]+$")
192+
193+
def_validate_agent_name(self, agent_name: str) ->None:
194+
"""Validate agent name to prevent arbitrary module imports."""
195+
# Strip the special agent prefix for validation
196+
ifagent_name.startswith("__"):
197+
name_to_check=agent_name[2:]
198+
check_dir=os.path.abspath(SPECIAL_AGENTS_DIR)
199+
else:
200+
name_to_check=agent_name
201+
check_dir=self.agents_dir
202+
203+
ifnotself._VALID_AGENT_NAME_RE.match(name_to_check):
204+
raiseValueError(
205+
f"Invalid agent name: {agent_name!r}. Agent names must be valid"
206+
" Python identifiers (letters, digits, and underscores only)."
207+
)
208+
209+
# Verify the agent exists on disk before allowing import
210+
agent_path=Path(check_dir) /name_to_check
211+
agent_file=Path(check_dir) /f"{name_to_check}.py"
212+
ifnot (agent_path.is_dir() oragent_file.is_file()):
213+
raiseValueError(
214+
f"Agent not found: {agent_name!r}. No matching directory or module"
215+
f" exists in '{os.path.join(check_dir, name_to_check)}'."
216+
)
217+
190218
def_perform_load(self, agent_name: str) ->Union[BaseAgent, App]:
191219
"""Internal logic to load an agent"""
220+
self._validate_agent_name(agent_name)
192221
# Determine the directory to use for loading
193222
ifagent_name.startswith("__"):
194223
# Special agent: use special agents directory

‎tests/unittests/cli/utils/test_agent_loader.py‎

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,6 @@ def test_error_messages_use_os_sep_consistently(self):
299299
loader.load_agent(agent_name)
300300

301301
exc_info.match(re.escape(expected_path))
302-
exc_info.match(re.escape(f"{agent_name}{os.sep}root_agent.yaml"))
303-
exc_info.match(re.escape(f"<agents_dir>{os.sep}"))
304302

305303
deftest_agent_loader_with_mocked_windows_path(self, monkeypatch):
306304
"""Mock Path() to simulate Windows behavior and catch regressions.
@@ -333,19 +331,10 @@ def test_agent_not_found_error(self):
333331
withpytest.raises(ValueError) asexc_info:
334332
loader.load_agent("nonexistent_agent")
335333

336-
expected_msg_part_1="No root_agent found for 'nonexistent_agent'."
337-
expected_msg_part_2= (
338-
"Searched in 'nonexistent_agent.agent.root_agent',"
339-
" 'nonexistent_agent.root_agent' and"
340-
" 'nonexistent_agent/root_agent.yaml'."
334+
assert"Agent not found: 'nonexistent_agent'"instr(exc_info.value)
335+
assertos.path.join(agents_dir, "nonexistent_agent") instr(
336+
exc_info.value
341337
)
342-
expected_msg_part_3= (
343-
f"Ensure '{agents_dir}/nonexistent_agent' is structured correctly"
344-
)
345-
346-
assertexpected_msg_part_1instr(exc_info.value)
347-
assertexpected_msg_part_2instr(exc_info.value)
348-
assertexpected_msg_part_3instr(exc_info.value)
349338

350339
deftest_agent_without_root_agent_error(self):
351340
"""Test that appropriate error is raised when agent has no root_agent."""
@@ -567,26 +556,16 @@ def test_yaml_agent_not_found_error(self):
567556
"""Test that appropriate error is raised when YAML agent is not found."""
568557
withtempfile.TemporaryDirectory() astemp_dir:
569558
loader=AgentLoader(temp_dir)
570-
agents_dir=temp_dir# For use in the expected message string
559+
agents_dir=temp_dir
571560

572561
# Try to load nonexistent YAML agent
573562
withpytest.raises(ValueError) asexc_info:
574563
loader.load_agent("nonexistent_yaml_agent")
575564

576-
expected_msg_part_1="No root_agent found for 'nonexistent_yaml_agent'."
577-
expected_msg_part_2= (
578-
"Searched in 'nonexistent_yaml_agent.agent.root_agent',"
579-
" 'nonexistent_yaml_agent.root_agent' and"
580-
" 'nonexistent_yaml_agent/root_agent.yaml'."
565+
assert"Agent not found: 'nonexistent_yaml_agent'"instr(exc_info.value)
566+
assertos.path.join(agents_dir, "nonexistent_yaml_agent") instr(
567+
exc_info.value
581568
)
582-
expected_msg_part_3= (
583-
f"Ensure '{agents_dir}/nonexistent_yaml_agent' is structured"
584-
" correctly"
585-
)
586-
587-
assertexpected_msg_part_1instr(exc_info.value)
588-
assertexpected_msg_part_2instr(exc_info.value)
589-
assertexpected_msg_part_3instr(exc_info.value)
590569

591570
deftest_yaml_agent_invalid_yaml_error(self):
592571
"""Test that appropriate error is raised when YAML is invalid."""
@@ -778,20 +757,7 @@ def test_special_agent_not_found_error(self):
778757
withpytest.raises(ValueError) asexc_info:
779758
loader.load_agent("__nonexistent_special")
780759

781-
expected_msg_part_1="No root_agent found for '__nonexistent_special'."
782-
expected_msg_part_2= (
783-
"Searched in 'nonexistent_special.agent.root_agent',"
784-
" 'nonexistent_special.root_agent' and"
785-
" 'nonexistent_special/root_agent.yaml'."
786-
)
787-
expected_msg_part_3= (
788-
f"Ensure '{special_agents_dir}/nonexistent_special' is structured"
789-
" correctly"
790-
)
791-
792-
assertexpected_msg_part_1instr(exc_info.value)
793-
assertexpected_msg_part_2instr(exc_info.value)
794-
assertexpected_msg_part_3instr(exc_info.value)
760+
assert"Agent not found: '__nonexistent_special'"instr(exc_info.value)
795761

796762
finally:
797763
# Restore original SPECIAL_AGENTS_DIR
@@ -1038,3 +1004,50 @@ def __init__(self):
10381004
assert"random_folder"notinagents
10391005
assert"data"notinagents
10401006
assert"tmp"notinagents
1007+
1008+
deftest_validate_agent_name_rejects_dotted_paths(self):
1009+
"""Agent names with dots are rejected to prevent arbitrary module imports."""
1010+
withtempfile.TemporaryDirectory() astemp_dir:
1011+
loader=AgentLoader(temp_dir)
1012+
fornamein ["os.path", "sys.modules", "subprocess.call"]:
1013+
withpytest.raises(ValueError, match="Invalid agent name"):
1014+
loader.load_agent(name)
1015+
1016+
deftest_validate_agent_name_rejects_relative_imports(self):
1017+
"""Agent names starting with dots are rejected."""
1018+
withtempfile.TemporaryDirectory() astemp_dir:
1019+
loader=AgentLoader(temp_dir)
1020+
fornamein ["..foo", ".bar", "...baz"]:
1021+
withpytest.raises(ValueError, match="Invalid agent name"):
1022+
loader.load_agent(name)
1023+
1024+
deftest_validate_agent_name_rejects_path_separators(self):
1025+
"""Agent names with slashes or special characters are rejected."""
1026+
withtempfile.TemporaryDirectory() astemp_dir:
1027+
loader=AgentLoader(temp_dir)
1028+
fornamein ["foo/bar", "foo\\bar", "foo-bar", "foo bar"]:
1029+
withpytest.raises(ValueError, match="Invalid agent name"):
1030+
loader.load_agent(name)
1031+
1032+
deftest_validate_agent_name_allows_valid_names(self):
1033+
"""Valid Python identifiers that exist on disk pass validation."""
1034+
withtempfile.TemporaryDirectory() astemp_dir:
1035+
temp_path=Path(temp_dir)
1036+
fornamein ["my_agent", "Agent1", "_private"]:
1037+
(temp_path/name).mkdir(exist_ok=True)
1038+
loader=AgentLoader(temp_dir)
1039+
fornamein ["my_agent", "Agent1", "_private"]:
1040+
# Should not raise ValueError for name validation;
1041+
# may raise other errors because the agent has no root_agent
1042+
withpytest.raises(Exception) asexc_info:
1043+
loader.load_agent(name)
1044+
assert"Invalid agent name"notinstr(exc_info.value)
1045+
assert"Agent not found"notinstr(exc_info.value)
1046+
1047+
deftest_validate_agent_name_rejects_nonexistent_agent(self):
1048+
"""Valid identifiers that don't exist on disk are rejected before import."""
1049+
withtempfile.TemporaryDirectory() astemp_dir:
1050+
loader=AgentLoader(temp_dir)
1051+
# 'subprocess' is a valid identifier but shouldn't be importable as an agent
1052+
withpytest.raises(ValueError, match="Agent not found"):
1053+
loader.load_agent("subprocess")

0 commit comments

Comments
 (0)