fix(agents): default-deny args keys in from_config() YAML loads#5533
fix(agents): default-deny args keys in from_config() YAML loads#5533vexlorn wants to merge 2 commits intogoogle:mainfrom
Conversation
`from_config()` now requires explicit `trusted=True` to allow `args` keys in YAML agent configs. Default behavior rejects `args` before it reaches the `resolve_code_reference` sink that calls the named callable. External-facing call sites (`cli_deploy.py` Agent Engine deployment template, `cli/utils/agent_loader.py` `adk run`/`adk web`/`adk deploy`) inherit the secure default with no caller-side change. Operator-controlled trusted loaders pass `trusted=True` explicitly. Legacy `_set_enforce_denylist(True)` flag is preserved as a force-on override for callers that already depend on it; `False` is now a no-op since the denylist is default-on. Refs google#5532
|
Setup: Python 3.14.4 / Win32, fresh venv per scenario. Workaround on
|
| Config | Call | Result |
|---|---|---|
LlmAgent, no args anywhere |
from_config(p) |
Loads, agent constructed |
model_code: LiteLlm with args: [{name: model, value: ...}] |
from_config(p, trusted=True) |
Loads, LiteLlm instantiated |
output_schema: {name: os.system, args: [...]} |
from_config(p) |
ValueError before resolution, no marker |
| Same exploit payload | from_config(p, trusted=True) |
Sink fires, marker created, pydantic error follows |
T1 covers the common case. T2 keeps operator-authored callable args working when the caller explicitly opts in. T3 is the issue scenario. T4 makes the opt-in nature explicit — trusted=True is a deliberate hand-off, not a silent bypass.
|
Hi @vexlorn , Thank you for your contribution! We appreciate you taking the time to submit this pull request. Your PR has been received by the team and is currently under review. We will provide feedback as soon as we have an update to share. |
|
Hi @Jacksunwei , can you please review this. |
Summary
from_config(config_path, *, trusted=False)rejects YAML configs containingargskeys by default_load_config_from_pathaccepts a matchingtrustedparameter; gate logic isif not trusted or _ENFORCE_DENYLISTcli_deploy.pyAgent Engine deployment template,cli/utils/agent_loader.py) inherit the secure default with no caller-side changetrusted=Trueexplicitly_set_enforce_denylist(True)preserved as a force-on override;_set_enforce_denylist(False)is a no-op since the default is now blockThis is one of the three fixes proposed in #5532. The other two (default-on of the existing global flag,
UntrustedCodeConfigtype split) are larger or breaking; this one is the smallest non-breaking change that closes the sink at the boundary.Threat model
config_agent_utils.from_config()and its private callee_load_config_from_path()reachresolve_code_reference(), which imports a Python callable named in YAML and invokes it with attacker-supplied positional and keyword arguments when the YAML contains anargskey. v1.31.1 added_check_yaml_for_blocked_keysbut gated it behind an opt-in flag that is not set by the call sites listed below.Reachable from:
src/google/adk/cli/cli_deploy.py:113— Agent Engine deployment template:root_agent = config_agent_utils.from_config(config_path)src/google/adk/cli/utils/agent_loader.py:173—_load_from_yaml_config()foradk run,adk web,adk deployAfter this PR, both call sites use the default
trusted=Falseand the denylist runs.Test plan
test_agent_config_litellm_model_with_custom_args— updated to passtrusted=True(operator-authored model_code with class-constructor args; legitimate)test_agent_config_legacy_model_mapping_still_supported— updated to passtrusted=True(same legitimate pattern, legacy field mapping)test_load_config_from_path_blocks_args_when_enforced— unchanged; legacy global flag still triggers blocktest_from_config_blocks_args_by_default— new; defaultfrom_config()rejectsargstest_from_config_allows_args_when_trusted— new;trusted=Trueaccepts the same YAMLtest_from_config_default_blocks_os_system_in_output_schema— new; concrete RCE PoC blocked, marker file not createdAll 35 tests in
tests/unittests/agents/test_agent_config.pypass locally (pytest -v).Out of scope
resolve_agent_reference()(sub-agent loading) does not currently propagatetrusted. If the parent istrusted=True, sub-agents loaded viaconfig_pathrevert to defaulttrusted=False. Maintainer to decide propagation policy.UntrustedCodeConfigtype split (issue option 3) — separate larger PR._set_enforce_denylistAPI — separate cleanup.Disclosure
Reported via g.co/vulnz on 2026-04-18 (Issue Tracker #503880658). Closed Won't Fix (Intended Behavior) on 2026-04-28. Filing per maintainer guidance to pursue fix via public channels.
Refs #5532.