Skip to content

Path traversal in sandbox Environments: model-supplied .. paths escape the workspace (Daytona, E2B, OpenSandbox, AgentEngine) #7699

Open

Description

@Subhan1794

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/passwd vs REJECTED
  • subdir/../../etc/shadow -> /workspace/subdir/../../etc/shadow vs 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 subsequent Execute calls 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

  1. xxwtiancai commented on Oct 23, 2026

    @xxwtiancai

    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.py that 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 raises ValueError when the normalized path escapes the root, with the same error wording as LocalEnvironment. All four environments route their _resolve_path through it. Absolute-path passthrough stays as-is per the ABC contract noted in your framing; whether to also contain absolute paths like LocalEnvironment does 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.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions