Skip to content

fix(agents): default-deny args keys in from_config() YAML loads#5533

Open
vexlorn wants to merge 2 commits intogoogle:mainfrom
vexlorn:fix/codeconfig-args-default-denylist
Open

fix(agents): default-deny args keys in from_config() YAML loads#5533
vexlorn wants to merge 2 commits intogoogle:mainfrom
vexlorn:fix/codeconfig-args-default-denylist

Conversation

@vexlorn
Copy link
Copy Markdown

@vexlorn vexlorn commented Apr 28, 2026

Summary

  • from_config(config_path, *, trusted=False) rejects YAML configs containing args keys by default
  • _load_config_from_path accepts a matching trusted parameter; gate logic is if not trusted or _ENFORCE_DENYLIST
  • External-facing call sites (cli_deploy.py Agent Engine deployment template, cli/utils/agent_loader.py) inherit the secure default with no caller-side change
  • Operator-controlled trusted loaders pass trusted=True explicitly
  • Legacy _set_enforce_denylist(True) preserved as a force-on override; _set_enforce_denylist(False) is a no-op since the default is now block

This is one of the three fixes proposed in #5532. The other two (default-on of the existing global flag, UntrustedCodeConfig type 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() reach resolve_code_reference(), which imports a Python callable named in YAML and invokes it with attacker-supplied positional and keyword arguments when the YAML contains an args key. v1.31.1 added _check_yaml_for_blocked_keys but 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() for adk run, adk web, adk deploy

After this PR, both call sites use the default trusted=False and the denylist runs.

Test plan

  • test_agent_config_litellm_model_with_custom_args — updated to pass trusted=True (operator-authored model_code with class-constructor args; legitimate)
  • test_agent_config_legacy_model_mapping_still_supported — updated to pass trusted=True (same legitimate pattern, legacy field mapping)
  • test_load_config_from_path_blocks_args_when_enforced — unchanged; legacy global flag still triggers block
  • test_from_config_blocks_args_by_default — new; default from_config() rejects args
  • test_from_config_allows_args_when_trusted — new; trusted=True accepts the same YAML
  • test_from_config_default_blocks_os_system_in_output_schema — new; concrete RCE PoC blocked, marker file not created

All 35 tests in tests/unittests/agents/test_agent_config.py pass locally (pytest -v).

Out of scope

  • resolve_agent_reference() (sub-agent loading) does not currently propagate trusted. If the parent is trusted=True, sub-agents loaded via config_path revert to default trusted=False. Maintainer to decide propagation policy.
  • UntrustedCodeConfig type split (issue option 3) — separate larger PR.
  • Removal of legacy _set_enforce_denylist API — 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.

`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
@adk-bot adk-bot added the core [Component] This issue is related to the core interface and implementation label Apr 28, 2026
@rohityan rohityan self-assigned this Apr 30, 2026
@vexlorn
Copy link
Copy Markdown
Author

vexlorn commented May 8, 2026

Setup: Python 3.14.4 / Win32, fresh venv per scenario.

Workaround on google-adk==1.32.0

Default from_config() against the output_schema: {name: os.system, args: [...]} payload from #5532:

  • Marker file appears at the path passed to os.system.
  • pydantic.ValidationError raises at the LlmAgent constructor, after the sink ran.

Same payload after _set_enforce_denylist(True):

  • No marker.
  • ValueError: Blocked key 'args' found in '<path>'. The 'args' field is not allowed in agent configurations because it can execute arbitrary code. raises before any callable resolution.

The workaround blocks the PoC. Since the flag is module-global, it would need to be set explicitly at every entry point (adk run, adk web, adk deploy, Agent Engine deployment templates) for the protection to apply by default. This PR closes that gap by gating on the call argument.

Unit tests on this branch

pytest tests/unittests/agents/test_agent_config.py -v against fix/codeconfig-args-default-denylist (HEAD 9b8ea68):

35 passed, 2 warnings in 2.82s

The four added cases all pass:

  • test_load_config_from_path_blocks_args_when_enforced
  • test_from_config_blocks_args_by_default
  • test_from_config_allows_args_when_trusted
  • test_from_config_default_blocks_os_system_in_output_schema

Hand-written configs

Four YAMLs against the same install, to make sure normal configs still load and the gate behaves as the PR claims:

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.

@rohityan rohityan requested a review from Jacksunwei May 8, 2026 23:18
@rohityan rohityan added the needs review [Status] The PR/issue is awaiting review from the maintainer label May 8, 2026
@rohityan
Copy link
Copy Markdown
Collaborator

rohityan commented May 8, 2026

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.

@rohityan
Copy link
Copy Markdown
Collaborator

rohityan commented May 8, 2026

Hi @Jacksunwei , can you please review this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core [Component] This issue is related to the core interface and implementation needs review [Status] The PR/issue is awaiting review from the maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants