From 924d802f5bb232a294df8e5a5c036d000077e19a Mon Sep 17 00:00:00 2001 From: prasanna8585 <65734642+prasanna8585@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:12:16 -0700 Subject: [PATCH] fix: block yaml and ruamel deserialization in agent-config code references Merge https://github.com/google/adk-python/pull/6646 Reject yaml and ruamel unsafe/full loaders in config agent to prevent RCE. Note: ruamel is a transitive dev/test dependency, but is blocked to prevent RCE if present in the environment. PiperOrigin-RevId: 967340296 --- src/google/adk/agents/config_agent_utils.py | 17 ++++++ tests/unittests/agents/test_agent_config.py | 65 +++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/google/adk/agents/config_agent_utils.py b/src/google/adk/agents/config_agent_utils.py index d0046edd..ea9c6e59 100644 --- a/src/google/adk/agents/config_agent_utils.py +++ b/src/google/adk/agents/config_agent_utils.py @@ -216,6 +216,23 @@ _BLOCKED_MODULES = frozenset({ "_testcapi", "_testinternalcapi", "test", + # Hard, always-installed third-party dependencies of adk-python itself + # (or common transitive dependencies) that ship exec-capable + # deserialization entry points. A denylist still cannot cover third-party + # packages in general (the loader resolves them by name, and any of the + # many packages an integration might install could have its own gadget), + # but these are common enough that they are blocked outright rather than + # left to the general third-party gap. + # + # yaml.unsafe_load (and yaml.load without an explicit safe Loader, and + # yaml.full_load) and ruamel.yaml equivalents construct arbitrary Python + # objects from the YAML document they are given, via tags such as + # !!python/object/apply:os.system. A reference to any of these as a + # code-reference field's target -- with the YAML content supplied as + # the function's argument at call time -- is a direct RCE primitive + # requiring no other preconditions. + "ruamel", + "yaml", }) diff --git a/tests/unittests/agents/test_agent_config.py b/tests/unittests/agents/test_agent_config.py index 1012b39e..bde1b1c0 100644 --- a/tests/unittests/agents/test_agent_config.py +++ b/tests/unittests/agents/test_agent_config.py @@ -697,6 +697,71 @@ def test_third_party_module_reference_is_not_blocked(): assert result is BaseModel +# yaml is a hard, always-installed dependency of adk-python itself (not an +# optional integration), and ruamel is a common transitive dependency. +# Both ship exec-capable deserialization entry points. +_YAML_UNSAFE_LOADER_REFS = [ + "yaml.unsafe_load", + "yaml.load", + "yaml.full_load", +] + +_RUAMEL_UNSAFE_LOADER_REFS = [ + "ruamel.yaml.round_trip_load", +] + + +@pytest.mark.parametrize( + "blocked_ref", _YAML_UNSAFE_LOADER_REFS + _RUAMEL_UNSAFE_LOADER_REFS +) +def test_resolve_code_reference_blocks_yaml_and_ruamel_deserialization( + blocked_ref: str, +): + """yaml and ruamel's unsafe/full loaders are rejected as code references.""" + with pytest.raises(ValueError, match="Blocked module reference"): + config_agent_utils.resolve_code_reference(CodeConfig(name=blocked_ref)) + + +@pytest.mark.parametrize( + "blocked_ref", _YAML_UNSAFE_LOADER_REFS + _RUAMEL_UNSAFE_LOADER_REFS +) +def test_resolve_tools_blocks_yaml_and_ruamel_deserialization(blocked_ref: str): + """yaml and ruamel's unsafe/full loaders are rejected as user-defined tools.""" + from google.adk.tools.tool_configs import ToolConfig + + tool_config = ToolConfig(name=blocked_ref) + with pytest.raises(ValueError, match="Blocked module reference"): + LlmAgent._resolve_tools([tool_config], "/fake/path.yaml") + + +_YAML_SAFE_LOOKING_REFS = [ + "yaml.safe_load", + "yaml.dump", + "yaml.SafeLoader", +] + +_RUAMEL_SAFE_LOOKING_REFS = [ + "ruamel.yaml.safe_load", + "ruamel.yaml.dump", +] + + +@pytest.mark.parametrize( + "blocked_ref", _YAML_SAFE_LOOKING_REFS + _RUAMEL_SAFE_LOOKING_REFS +) +def test_harmless_looking_yaml_and_ruamel_references_are_also_blocked( + blocked_ref: str, +): + """The whole yaml and ruamel modules are off-limits, not just the scary parts. + + Blocking them in full locks in the module-wide intent and prevents + future bypasses if new unsafe loaders are added or if safe-looking names + are refactored to resolve differently. + """ + with pytest.raises(ValueError, match="Blocked module reference"): + config_agent_utils.resolve_code_reference(CodeConfig(name=blocked_ref)) + + def test_denylist_can_be_disabled(): """Verify _set_enforce_denylist(False) disables module blocking.""" config_agent_utils._set_enforce_denylist(False)