Skip to content

Commit 8cebb64

Browse files
committed
security: add module blocklist for YAML agent config code references
1 parent 4006fe4 commit 8cebb64

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

src/google/adk/agents/config_agent_utils.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,76 @@ def _load_config_from_path(config_path: str) -> AgentConfig:
105105
return AgentConfig.model_validate(config_data)
106106

107107

108+
_ENFORCE_DENYLIST = False
109+
110+
# Modules that must never be imported via YAML agent configuration.
111+
# These provide direct access to the operating system, process execution,
112+
# or dynamic code evaluation and could be abused to achieve arbitrary
113+
# code execution when referenced in callback, tool, schema, or model
114+
# code-reference fields.
115+
_BLOCKED_MODULES = frozenset({
116+
"os",
117+
"subprocess",
118+
"sys",
119+
"builtins",
120+
"importlib",
121+
"shutil",
122+
"socket",
123+
"http",
124+
"urllib",
125+
"ctypes",
126+
"multiprocessing",
127+
"threading",
128+
"signal",
129+
"code",
130+
"codeop",
131+
"compileall",
132+
"runpy",
133+
"webbrowser",
134+
"antigravity",
135+
"pty",
136+
"commands",
137+
"pdb",
138+
"profile",
139+
"tempfile",
140+
"shelve",
141+
"pickle",
142+
"marshal",
143+
})
144+
145+
146+
def _validate_module_reference(fully_qualified_name: str) -> None:
147+
"""Validate that a module reference does not target a blocked module.
148+
149+
Args:
150+
fully_qualified_name: The fully-qualified Python name to validate
151+
(e.g. ``"my_package.my_module.my_func"``).
152+
153+
Raises:
154+
ValueError: If the top-level module is in ``_BLOCKED_MODULES``.
155+
"""
156+
if not _ENFORCE_DENYLIST:
157+
return
158+
# Extract the top-level package from the fully-qualified name.
159+
top_module = fully_qualified_name.split(".")[0]
160+
if top_module in _BLOCKED_MODULES:
161+
raise ValueError(
162+
f"Blocked module reference: {fully_qualified_name!r}. "
163+
f"Importing from the '{top_module}' module is not allowed in "
164+
"agent configurations because it can execute arbitrary code."
165+
)
166+
167+
168+
def _set_enforce_denylist(value: bool) -> None:
169+
global _ENFORCE_DENYLIST
170+
_ENFORCE_DENYLIST = value
171+
172+
108173
@experimental(FeatureName.AGENT_CONFIG)
109174
def resolve_fully_qualified_name(name: str) -> Any:
110175
try:
111176
module_path, obj_name = name.rsplit(".", 1)
177+
_validate_module_reference(name)
112178
module = importlib.import_module(module_path)
113179
return getattr(module, obj_name)
114180
except Exception as e:
@@ -160,6 +226,7 @@ def _resolve_agent_code_reference(code: str) -> Any:
160226
if "." not in code:
161227
raise ValueError(f"Invalid code reference: {code}")
162228

229+
_validate_module_reference(code)
163230
module_path, obj_name = code.rsplit(".", 1)
164231
module = importlib.import_module(module_path)
165232
obj = getattr(module, obj_name)
@@ -189,6 +256,7 @@ def resolve_code_reference(code_config: CodeConfig) -> Any:
189256
if not code_config or not code_config.name:
190257
raise ValueError("Invalid CodeConfig.")
191258

259+
_validate_module_reference(code_config.name)
192260
module_path, obj_name = code_config.name.rsplit(".", 1)
193261
module = importlib.import_module(module_path)
194262
return getattr(module, obj_name)

tests/unittests/agents/test_agent_config.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,63 @@ def fake_from_config(path: str):
465465
)
466466
assert result == "sentinel"
467467
assert recorded["path"] == expected_path
468+
469+
470+
# --- Security tests: module blocklist for YAML agent config code references ---
471+
472+
473+
def test_resolve_code_reference_blocks_os_when_enforced():
474+
"""Verify resolve_code_reference blocks os module directly."""
475+
from google.adk.agents.common_configs import CodeConfig
476+
477+
config_agent_utils._set_enforce_denylist(True)
478+
try:
479+
with pytest.raises(ValueError, match="Blocked module reference"):
480+
config_agent_utils.resolve_code_reference(
481+
CodeConfig(name="os.system")
482+
)
483+
finally:
484+
config_agent_utils._set_enforce_denylist(False)
485+
486+
487+
def test_resolve_fully_qualified_name_blocks_subprocess_when_enforced():
488+
"""Verify resolve_fully_qualified_name blocks subprocess module."""
489+
config_agent_utils._set_enforce_denylist(True)
490+
try:
491+
with pytest.raises(ValueError, match="Blocked module reference"):
492+
config_agent_utils.resolve_fully_qualified_name("subprocess.Popen")
493+
finally:
494+
config_agent_utils._set_enforce_denylist(False)
495+
496+
497+
def test_allowed_module_passes_when_enforced(tmp_path: Path):
498+
"""Verify that google.adk modules are NOT blocked by the module denylist."""
499+
config_agent_utils._set_enforce_denylist(True)
500+
try:
501+
# This should NOT raise — google.adk modules must remain allowed
502+
result = config_agent_utils.resolve_fully_qualified_name(
503+
"google.adk.agents.llm_agent.LlmAgent"
504+
)
505+
assert result is LlmAgent
506+
finally:
507+
config_agent_utils._set_enforce_denylist(False)
508+
509+
510+
@pytest.mark.parametrize(
511+
"blocked_module",
512+
[
513+
"os.system",
514+
"subprocess.call",
515+
"builtins.exec",
516+
],
517+
)
518+
def test_resolve_agent_code_reference_blocks_when_enforced(
519+
blocked_module: str,
520+
):
521+
"""Verify _resolve_agent_code_reference blocks dangerous modules."""
522+
config_agent_utils._set_enforce_denylist(True)
523+
try:
524+
with pytest.raises(ValueError, match="Blocked module reference"):
525+
config_agent_utils._resolve_agent_code_reference(blocked_module)
526+
finally:
527+
config_agent_utils._set_enforce_denylist(False)

0 commit comments

Comments
 (0)