Repository navigation
Path traversal in sandbox Environments: model-supplied .. paths escape the workspace (Daytona, E2B, OpenSandbox, AgentEngine) #7699
Description
Summary
The four remote sandbox Environment implementations in google/adk-python share an identical _resolve_path with no .. normalization and no containment check, while the model-facing file tools validate nothing but non-emptiness. A model steered by untrusted input can therefore read and write files outside the workspace inside the sandbox VM. The reference LocalEnvironment enforces containment (raises on escape), proving this gap is a defect, not the design.
Affected code
All four share this verbatim resolver (no normalization, no containment, no symlink resolution):
def _resolve_path(self, path):
pure = PurePosixPath(os.fspath(path))
if pure.is_absolute():
return str(pure)
return str(PurePosixPath(_SANDBOX_HOME) / pure)
src/google/adk/integrations/daytona/_daytona_environment.py:243(_SANDBOX_HOME = "/workspaces")src/google/adk/integrations/e2b/_e2b_environment.py:186(_SANDBOX_HOME = "/home/user")src/google/adk/integrations/opensandbox/_opensandbox_environment.py:386(_SANDBOX_HOME = "/workspace")src/google/adk/integrations/vmaas/_agent_engine_environment.py:304(_WORKSPACE = "/workspace")
Model-facing entry points (no validation of their own):
src/google/adk/tools/environment/_read_file_tool.py(ReadFile: only checks path is non-empty)src/google/adk/tools/environment/_write_file_tool.py(WriteFile: only checks path is non-empty)src/google/adk/tools/environment/_edit_file_tool.py(EditFile: read + write through the same path)- Bundled for the model by
src/google/adk/tools/environment/_environment_toolset.py(EnvironmentToolset: ReadFile + EditFile + WriteFile + Execute).
Evidence
Resolver behavior on identical inputs (sandbox resolver vs LocalEnvironment):
notes.txt->/workspace/notes.txt(inside) vs inside workdir (ok)/etc/passwd->/etc/passwd(passthrough) vs REJECTED../../etc/passwd->/workspace/../../etc/passwdvs REJECTEDsubdir/../../etc/shadow->/workspace/subdir/../../etc/shadowvs REJECTED..->/workspace/..vs REJECTED
/workspace/../../etc/passwd normalizes (POSIX normpath, and by the kernel at open() time) to /etc/passwd. The vmaas path goes through a shell, the other three through their SDK file APIs — all resolve through OS path resolution, which collapses .. and honors absolute paths. The Daytona Python SDK (daytona==0.140.0, inspected) performs NO client-side path validation or containment: FileSystem.download_file(remote_path) passes the path verbatim into FileDownloadRequest(source=remote_path), and the toolbox API reads it via OS calls. So nothing between ADK's _resolve_path and the sandbox kernel re-scopes the path.
Impact (inside the sandbox VM)
- Read: image-baked secrets, other workspaces/skills' files, any file the sandbox user can read.
- Write: overwrite shell startup files (e.g.
~/.bashrc) -> persistent code execution on subsequentExecutecalls inside the sandbox; overwrite other skills' scripts / agent files staged in the sandbox. - NOT a host escape: the sandbox VM boundary itself is intact. Severity: Medium (workspace-confinement bypass).
Framing note
The BaseEnvironment ABC docstring describes paths as "Absolute or working-dir-relative", so absolute-path passthrough is arguably by design at the ABC level. This finding is framed on the .. traversal, which no reading of that contract permits (it lands outside the working directory). Absolute passthrough is noted as context only.
Limitation (stated plainly)
No live credentialed sandbox run was possible here (no Daytona/E2B/OpenSandbox/Agent-Engine API credentials available), so proof is at the resolver level (verbatim functions executed locally) plus POSIX path-resolution semantics and the Daytona SDK source. A live-sandbox end-to-end demo still needs a credentialed sandbox run. The toolbox server side was not inspected for additional validation.
Suggested fix shape (not applied)
Resolve like LocalEnvironment does: normalize the candidate, resolve symlinks, and raise when the resolved path is not inside the working directory — applied in all four remote environments.
Dup-screen note
Searched for a prior Python ADK Environment _resolve_path traversal report and found none (only an unrelated third-party daytona-adk-plugin reference). Distinct from CVE-2026-105772 (adk-JS FileArtifactService traversal — different language/service) and CVE-2026-79696 (adk web test-replay denylist RCE).
Activity
xxwtiancai commentedon Oct 23, 2026
I'd like to take this one — thank you for the exceptionally clear report.
Approach (mirroring
LocalEnvironment's containment for the remote case): a shared resolver helper in_base_environment.pythat anchors relative paths at the sandbox workspace root, normalizes them lexically (the filesystem that would resolve symlinks lives inside the sandbox, so client-side symlink resolution isn't possible — documented as such), and raisesValueErrorwhen the normalized path escapes the root, with the same error wording asLocalEnvironment. All four environments route their_resolve_paththrough it. Absolute-path passthrough stays as-is per the ABC contract noted in your framing; whether to also contain absolute paths likeLocalEnvironmentdoes feels like a separate, deliberate contract change, so I've left it out and flagged it in the PR.Resolver-level unit tests per environment plus helper contract tests; no live-sandbox run on my side either. PR up in a moment.