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:
- A symlink is created inside
PROJECT_ROOT pointing to a sensitive file outside it (e.g., ~/.ssh/id_rsa, ~/.aws/credentials, /etc/passwd)
- The agent calls any coder tool (read_file, write_file, etc.) referencing the symlink path
_resolve_path() sees the symlink is lexically inside PROJECT_ROOT — the commonpath check passes
- 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() |
Describe the Bug
In
tools/coder_tools_server.py,_resolve_path()usesos.path.abspath()to resolve paths and then checks containment withos.path.commonpath(). Sinceabspath()does not resolve symlinks, a symlink insidePROJECT_ROOTpointing to a target outside it will pass the containment check — allowing the agent to read/write files outside the project sandbox._resolve_pathis 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:
PROJECT_ROOTpointing to a sensitive file outside it (e.g.,~/.ssh/id_rsa,~/.aws/credentials,/etc/passwd)_resolve_path()sees the symlink is lexically insidePROJECT_ROOT— thecommonpathcheck passesImpact
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 usesos.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():This mirrors the approach already used in
get_secure_path(). No new dependencies — justos.path.realpathfrom stdlib.Files to Modify
tools/coder_tools_server.pyos.path.realpath()containment check to_resolve_path()