Skip to content

[Bug]: _resolve_path() symlink escape bypasses PROJECT_ROOT sandbox in coder_tools_server.py #6788

@genyarko

Description

@genyarko

Describe the Bug

In tools/coder_tools_server.py, _resolve_path() uses os.path.abspath() to resolve paths and then checks containment with os.path.commonpath(). Since abspath() does not resolve symlinks, a symlink inside PROJECT_ROOT pointing to a target outside it will pass the containment check — allowing the agent to read/write files outside the project sandbox.

def _resolve_path(path: str) -> str:
    """Resolve path relative to PROJECT_ROOT. Raises ValueError if outside."""
    path = path.replace("/", os.sep)
    if os.path.isabs(path):
        resolved = os.path.abspath(path)  # does NOT resolve symlinks
        # ...
    else:
        resolved = os.path.abspath(os.path.join(PROJECT_ROOT, path))  # same issue

    common = os.path.commonpath([resolved, PROJECT_ROOT])
    if common != PROJECT_ROOT:
        raise ValueError(...)
    return resolved  # symlink target may be outside PROJECT_ROOT

_resolve_path is called at 6 sites in coder_tools_server.py — it gates every file read, write, and code operation the agent performs through the coder tools MCP server.

Why This Is a Problem

Attack scenario:

  1. A symlink is created inside PROJECT_ROOT pointing to a sensitive file outside it (e.g., ~/.ssh/id_rsa, ~/.aws/credentials, /etc/passwd)
  2. The agent calls any coder tool (read_file, write_file, etc.) referencing the symlink path
  3. _resolve_path() sees the symlink is lexically inside PROJECT_ROOT — the commonpath check passes
  4. The agent reads or writes files outside the project sandbox

Impact

  • Sandbox escape: The agent can access arbitrary files on the host filesystem by following symlinks
  • Data exfiltration: Sensitive credentials, keys, and config files outside the project can be read
  • Arbitrary write: Files outside the project can be overwritten if a symlink points to them

Context

This is the same class of bug that was already fixed in get_secure_path() (tools/src/aden_tools/tools/file_system_toolkits/security.py), which now correctly uses os.path.realpath(). The fix was not applied to _resolve_path().

Proposed Fix

Add an os.path.realpath() check after the existing lexical containment check in _resolve_path():

    # Existing lexical check passes — now verify the real target
    real_resolved = os.path.realpath(resolved)
    real_root = os.path.realpath(PROJECT_ROOT)
    try:
        real_common = os.path.commonpath([real_resolved, real_root])
    except ValueError:
        real_common = ""
    if real_common != real_root:
        raise ValueError(f"Access denied: '{path}' resolves to a symlink target outside the project root.")

    return resolved

This mirrors the approach already used in get_secure_path(). No new dependencies — just os.path.realpath from stdlib.

Files to Modify

File Change
tools/coder_tools_server.py Add os.path.realpath() containment check to _resolve_path()

Metadata

Metadata

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