Skip to content

Commit 2c73ecd

Browse files
committed
security: add module blocklist for YAML agent config code references
Add a _BLOCKED_MODULES set and _validate_module_reference() function to prevent importing dangerous standard library modules (os, subprocess, builtins, importlib, pickle, etc.) when resolving code references from YAML agent configurations. The existing CVE-2026-4810 fix blocks the 'args' key in YAML configs to prevent passing arguments to constructors. However, the resolve_code_reference(), resolve_fully_qualified_name(), and _resolve_tools() functions still call importlib.import_module() with no restriction on which modules can be imported. This allows an attacker to reference dangerous callables like os.system or subprocess.call in callback, tool, schema, or model code-reference fields. This commit adds validation at all three import points: - resolve_code_reference() (used for callbacks, schemas, model_code) - resolve_fully_qualified_name() (used for agent class resolution) - _resolve_tools() (used for user-defined tool resolution) The blocklist is only enforced when _ENFORCE_DENYLIST is True (set by the web dev server), matching the existing denylist behavior. Includes comprehensive tests verifying: - 11 different blocked modules are rejected in callback fields - 3 blocked modules are rejected in tool fields - Direct resolve_code_reference() calls are blocked - Direct resolve_fully_qualified_name() calls are blocked - google.adk.* modules continue to work (allowlist behavior)
1 parent 5ad1942 commit 2c73ecd

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

src/google/adk/agents/config_agent_utils.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ def _resolve_tools(self, tool_configs: list[ToolConfig]) -> list[Any]:
158158
obj = getattr(module, tool_config.name)
159159
else:
160160
# User-defined tools
161+
_validate_module_reference(tool_config.name)
161162
module_path, obj_name = tool_config.name.rsplit(".", 1)
162163
module = importlib.import_module(module_path)
163164
obj = getattr(module, obj_name)
@@ -490,6 +491,63 @@ def _resolve_agent_class(agent_class: str) -> type[Any]:
490491
_BLOCKED_YAML_KEYS = frozenset({"args"})
491492
_ENFORCE_DENYLIST = False
492493

494+
# Modules that must never be imported via YAML agent configuration.
495+
# These provide direct access to the operating system, process execution,
496+
# or dynamic code evaluation and could be abused to achieve arbitrary
497+
# code execution when referenced in callback, tool, schema, or model
498+
# code-reference fields.
499+
_BLOCKED_MODULES = frozenset({
500+
"os",
501+
"subprocess",
502+
"sys",
503+
"builtins",
504+
"importlib",
505+
"shutil",
506+
"socket",
507+
"http",
508+
"urllib",
509+
"ctypes",
510+
"multiprocessing",
511+
"threading",
512+
"signal",
513+
"code",
514+
"codeop",
515+
"compileall",
516+
"runpy",
517+
"webbrowser",
518+
"antigravity",
519+
"pty",
520+
"commands",
521+
"pdb",
522+
"profile",
523+
"tempfile",
524+
"shelve",
525+
"pickle",
526+
"marshal",
527+
})
528+
529+
530+
def _validate_module_reference(fully_qualified_name: str) -> None:
531+
"""Validate that a module reference does not target a blocked module.
532+
533+
Args:
534+
fully_qualified_name: The fully-qualified Python name to validate
535+
(e.g. ``"my_package.my_module.my_func"``).
536+
537+
Raises:
538+
ValueError: If the top-level module is in ``_BLOCKED_MODULES``.
539+
"""
540+
if not _ENFORCE_DENYLIST:
541+
return
542+
# Extract the top-level package from the fully-qualified name.
543+
top_module = fully_qualified_name.split(".")[0]
544+
if top_module in _BLOCKED_MODULES:
545+
raise ValueError(
546+
f"Blocked module reference: {fully_qualified_name!r}. "
547+
f"Importing from the '{top_module}' module is not allowed in "
548+
"agent configurations because it can execute arbitrary code."
549+
)
550+
493551

494552
def _set_enforce_denylist(value: bool) -> None:
495553
global _ENFORCE_DENYLIST
@@ -516,8 +574,11 @@ def _check_config_for_blocked_keys(node: Any, filename: str) -> None:
516574
def resolve_fully_qualified_name(name: str) -> Any:
517575
try:
518576
module_path, obj_name = name.rsplit(".", 1)
577+
_validate_module_reference(name)
519578
module = importlib.import_module(module_path)
520579
return getattr(module, obj_name)
580+
except ValueError:
581+
raise
521582
except Exception as e:
522583
raise ValueError(f"Invalid fully qualified name: {name}") from e
523584

@@ -568,6 +629,7 @@ def _resolve_agent_code_reference(code: str) -> Any:
568629
raise ValueError(f"Invalid code reference: {code}")
569630

570631
module_path, obj_name = code.rsplit(".", 1)
632+
_validate_module_reference(code)
571633
module = importlib.import_module(module_path)
572634
obj = getattr(module, obj_name)
573635

@@ -596,6 +658,7 @@ def resolve_code_reference(code_config: CodeConfig) -> Any:
596658
if not code_config or not code_config.name:
597659
raise ValueError("Invalid CodeConfig.")
598660

661+
_validate_module_reference(code_config.name)
599662
module_path, obj_name = code_config.name.rsplit(".", 1)
600663
module = importlib.import_module(module_path)
601664
obj = getattr(module, obj_name)

tests/unittests/agents/test_agent_config.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,107 @@ def test_from_config_blocks_args_when_enforced(tmp_path):
373373
config_agent_utils._set_enforce_denylist(False)
374374

375375

376+
@pytest.mark.parametrize(
377+
"blocked_module",
378+
[
379+
"os.system",
380+
"subprocess.call",
381+
"subprocess.Popen",
382+
"builtins.exec",
383+
"builtins.eval",
384+
"importlib.import_module",
385+
"shutil.rmtree",
386+
"socket.socket",
387+
"ctypes.cdll",
388+
"pickle.loads",
389+
"marshal.loads",
390+
],
391+
)
392+
def test_blocked_module_in_callback_raises_when_enforced(
393+
blocked_module: str, tmp_path: Path
394+
):
395+
"""Verify that referencing blocked stdlib modules in callbacks is rejected."""
396+
config_file = tmp_path / "malicious.yaml"
397+
config_file.write_text(f"""\
398+
name: evil_agent
399+
model: gemini-2.0-flash
400+
instruction: "harmless"
401+
before_agent_callbacks:
402+
- name: "{blocked_module}"
403+
""")
404+
405+
config_agent_utils._set_enforce_denylist(True)
406+
try:
407+
with pytest.raises(ValueError, match="Blocked module reference"):
408+
config_agent_utils.from_config(str(config_file))
409+
finally:
410+
config_agent_utils._set_enforce_denylist(False)
411+
412+
413+
@pytest.mark.parametrize(
414+
"blocked_module",
415+
[
416+
"os.system",
417+
"subprocess.run",
418+
"builtins.exec",
419+
],
420+
)
421+
def test_blocked_module_in_tools_raises_when_enforced(
422+
blocked_module: str, tmp_path: Path
423+
):
424+
"""Verify that referencing blocked stdlib modules in tools is rejected."""
425+
config_file = tmp_path / "malicious.yaml"
426+
config_file.write_text(f"""\
427+
name: evil_agent
428+
model: gemini-2.0-flash
429+
instruction: "harmless"
430+
tools:
431+
- name: "{blocked_module}"
432+
""")
433+
434+
config_agent_utils._set_enforce_denylist(True)
435+
try:
436+
with pytest.raises(ValueError, match="Blocked module reference"):
437+
config_agent_utils.from_config(str(config_file))
438+
finally:
439+
config_agent_utils._set_enforce_denylist(False)
440+
441+
442+
def test_resolve_code_reference_blocks_os_when_enforced():
443+
"""Verify resolve_code_reference blocks os module directly."""
444+
from google.adk.agents.common_configs import CodeConfig
445+
446+
config_agent_utils._set_enforce_denylist(True)
447+
try:
448+
with pytest.raises(ValueError, match="Blocked module reference"):
449+
config_agent_utils.resolve_code_reference(CodeConfig(name="os.system"))
450+
finally:
451+
config_agent_utils._set_enforce_denylist(False)
452+
453+
454+
def test_resolve_fully_qualified_name_blocks_subprocess_when_enforced():
455+
"""Verify resolve_fully_qualified_name blocks subprocess module."""
456+
config_agent_utils._set_enforce_denylist(True)
457+
try:
458+
with pytest.raises(ValueError, match="Blocked module reference"):
459+
config_agent_utils.resolve_fully_qualified_name("subprocess.Popen")
460+
finally:
461+
config_agent_utils._set_enforce_denylist(False)
462+
463+
464+
def test_allowed_module_passes_when_enforced(tmp_path: Path):
465+
"""Verify that google.adk modules are NOT blocked by the module denylist."""
466+
config_agent_utils._set_enforce_denylist(True)
467+
try:
468+
# This should NOT raise — google.adk modules must remain allowed
469+
result = config_agent_utils.resolve_fully_qualified_name(
470+
"google.adk.agents.llm_agent.LlmAgent"
471+
)
472+
assert result is LlmAgent
473+
finally:
474+
config_agent_utils._set_enforce_denylist(False)
475+
476+
376477
def test_create_workflow_from_yaml(tmp_path: Path):
377478
"""Test creating a Workflow from a YAML file."""
378479
yaml_content = """

0 commit comments

Comments
 (0)