Skip to content

Path traversal in remote-template scaffolding → arbitrary file write (agents-cli scaffold create --agent <remote>) #50

Description

@Aravindargutus

What happened?

agents-cli scaffold create --agent clones an attacker-controlled Git repository and processes its agents-cli-manifest.yaml with no confirmation prompt. The manifest's settings.agent_directory value is used as a filesystem path component with no path-traversal validation whenever settings.language is anything other than python (i.e. go, java, or typescript).

Root cause: validate_agent_directory_name() in src/google/agents/cli/scaffold/utils/template.py returns immediately without performing any checks when language != "python":

template.py, validate_agent_directory_name()

if language != "python":
return
The unsanitized value then reaches a write sink in copy_flat_structure_agent_files():

agent_dst = dst / agent_directory # no traversal/absolute-path check
agent_dst.mkdir(parents=True, exist_ok=True)
...
shutil.copy2(item, agent_dst / item.name) # writes attacker's .py file here
Because agent_directory is taken verbatim from the attacker's manifest, a value like ../../../../../../tmp/whatever or an absolute path (/home/victim/.something) escapes the generated project directory entirely. The attacker also controls the file content (any .py file shipped in their repo), so this is a full arbitrary-path, attacker-controlled-content file write — not just a traversal read.

Steps to Reproduce

Create a malicious template repo, e.g. github.com/attacker/evil-agent, containing: agents-cli-manifest.yaml:
name: evil-agent
description: Totally legit agent template
base_template: adk
is_flat_structure: true
settings:
language: go
agent_directory: "../../../../../../../../tmp/acli_poc_escape"
agent.py:
print("PWNED: this ran from", file)
Victim runs:
agents-cli scaffold create victim-project --agent github.com/attacker/evil-agent --auto-approve
After completion, inspect /tmp/acli_poc_escape/ — it now contains agent.py with the attacker's exact content, written outside the project directory the victim asked for and outside the CLI's own temp scratch space (which is cleaned up afterward; this file is not). An absolute path in agent_directory (e.g. /tmp/acli_poc_abs_escape) works identically — no ../ is even required.
Verified locally by driving the unmodified production code path directly (remote_template.load_remote_template_config() → template.process_template()), bypassing only the network clone step:

language: go + traversal agent_directory → file written outside project dir ✅
language: typescript + absolute agent_directory → file written to absolute attacker path ✅
language: python + identical traversal payload → correctly rejected: ValueError: Agent directory '...' is not a valid Python identifier (confirms the python-only validation is the sole gate, and it is missing for the other three supported languages)

What did you expect to happen?

agent_directory should be validated identically regardless of language — rejecting /, , .., and absolute/drive-letter paths — and the final write path should be resolved and confirmed to stay within the generated project directory before any file is written, for all supported languages (python, go, java, typescript).

Client information

CLI version: 0.6.1
CLI install path: /Users/aravind-11556/.cache/uv/archive-v0/y69iTXkVnlpi4C4sqc9Ic/lib/python3.11/site-packages/google/agents/cli
OS info: macOS-26.5.1-arm64-arm-64bit
Installed skills: 7 (project)

google-agents-cli-adk-code
google-agents-cli-deploy
google-agents-cli-eval
google-agents-cli-observability
google-agents-cli-publish
google-agents-cli-scaffold
google-agents-cli-workflow

Command Output / Logs

$ python3 -c "... process_template(...) ..."
language: go | agent_directory: ../../../../../../../../tmp/acli_poc_escape

Intended project directory : .../victim_output/victim-project
exists: True

Escape target (OUTSIDE victim_output, OUTSIDE any tempdir): /tmp/acli_poc_escape
exists: True

*** VULNERABLE ***
Attacker-controlled file was written OUTSIDE the project directory at:
/tmp/acli_poc_escape/agent.py

Anything else we need to know?

Impact: Arbitrary file write with attacker-controlled path and content, triggered by simply scaffolding from an untrusted/community remote template — a documented, encouraged workflow (agents-cli scaffold create --agent github.com/org/repo, and the interactive "Browse → Custom URL" / google/adk-samples menu). No confirmation is shown before the manifest is parsed and acted on. Realistic exploitation: drop a malicious .py file into a directory on the victim's Python import path, into another project's source tree, or into a dotfile/config directory, to achieve code execution on a subsequent unrelated action.

Related issues found in the same code path (reported here for completeness, lower severity):

Symlink dereference on remote-template copy — copy_files() / copy_flat_structure_agent_files() use shutil.copy2/copytree with default symlink-following behavior. A template shipping a symlink (e.g. secrets -> ~/.ssh) gets the target's contents copied into the generated project, which a user could unknowingly commit/publish. Read-only disclosure, same untrusted-template trust boundary.
.env written world-readable — when --google-api-key is used, the generated .env containing the live API key is written via Path.write_text() with default perms (0644), not chmod(0o600).
Suggested fix:

Remove the language != "python" early-return in validate_agent_directory_name(); apply path-traversal/absolute-path rejection unconditionally.
After resolving agent_dst in copy_flat_structure_agent_files() (and the equivalent agent_directory-joined paths elsewhere in process_template()), assert agent_dst.resolve() is contained within the generated project directory before writing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions