#1568·miniserve

miniserve ZIP archive download follows symlink target outside served root even with `--no-symlinks`

Author: br0x2Created Jun 19, 2026Updated Jun 19, 2026

miniserve ZIP archive download follows symlink target outside served root even with --no-symlinks

Summary

The ZIP archive download path still follows symlinks and includes the target file contents in the generated archive. This also happens when miniserve is started with --no-symlinks, even though that option is documented as hiding symlinks and preventing them from being followed.

I fetched origin/master on 2026-06-18 and tested the latest upstream commit:

925fcfe2c64575a364580788749f5645fde0873e (2026-06-01T18:40:31+02:00)

Details

Relevant code path:

  • src/args.rs lines 163-165: --no-symlinks is documented as "Hide symlinks in listing and prevent them from being followed".
  • src/listing.rs lines 424-428: the archive-download handler passes conf.no_symlinks into archive_method.create_archive(...).
  • src/archive.rs lines 237-249: ZIP generation calls std::fs::metadata(entry_path), which follows symlinks, and then checks entry_metadata.file_type().is_symlink(). Because the metadata is for the target, not the symlink itself, this check does not detect the symlink.
  • src/archive.rs lines 274-289: the same entry_path is opened with File::open(&entry_path) and written into the ZIP as a regular file entry, so the outside target's contents are packaged.
  • src/archive.rs lines 148-155: the TAR path also follows symlinks by default through tar_builder.follow_symlinks(!skip_symlinks).

Root cause:

The archive generator treats a path inside the served directory as safe without validating what the path resolves to. In the ZIP path, std::fs::metadata() and File::open() both follow symlinks, so a symlink entry inside the served root can cause miniserve to read and package a file outside the served root. The skip_symlinks check is ineffective for ZIP because it is checking followed metadata rather than symlink metadata.

Related CVE reference used for triage: CVE-2024-12718 in python/cpython, as the same root-boundary pattern where archive/path handling must verify that resolved paths stay inside the intended root.

Reproduction

This reproducer uses a fresh checkout and starts miniserve with --no-symlinks:

bash
set -euo pipefail

git clone https://github.com/svenstaro/miniserve.git miniserve-repro
cd miniserve-repro
git checkout 925fcfe2c64575a364580788749f5645fde0873e
cargo build --locked --no-default-features

tmp="$(mktemp -d)"
cleanup() {
  kill "${pid:-}" 2>/dev/null || true
  rm -rf "$tmp"
}
trap cleanup EXIT

mkdir -p "$tmp/served"
printf 'MINISERVE-OUTSIDE-SECRET\n' > "$tmp/outside-secret.txt"
ln -s ../outside-secret.txt "$tmp/served/link_to_outside"

./target/debug/miniserve \
  --enable-zip \
  --no-symlinks \
  -i 127.0.0.1 \
  -p 18181 \
  -- "$tmp/served" > "$tmp/server.log" 2>&1 &
pid=$!

for _ in $(seq 1 50); do
  curl -fsS http://127.0.0.1:18181/ >/dev/null 2>&1 && break
  sleep 0.1
done

curl -fsS -o "$tmp/download.zip" 'http://127.0.0.1:18181/?download=zip'

unzip -l "$tmp/download.zip" | grep 'served/link_to_outside'
unzip -p "$tmp/download.zip" served/link_to_outside

Expected Behavior

With --no-symlinks, archive generation should not follow a symlink inside the served directory. More generally, archive downloads should not include file contents whose resolved path is outside the served root.

Observed Behavior

The ZIP archive contains the symlink path as a regular file entry, and reading that ZIP entry returns the contents of the file outside the served root:

served/link_to_outside
MINISERVE-OUTSIDE-SECRET

I also verified the default TAR download path with --enable-tar and no --no-symlinks; the generated TAR contains served/link_to_outside as a regular file and tar -xOf returns MINISERVE-OUTSIDE-SECRET.

Impact

If a served directory contains a symlink to a file outside the shared root, a remote downloader can request a generated archive and receive the outside file's contents. This is especially surprising for ZIP downloads because the behavior still occurs when the operator starts miniserve with --no-symlinks.

Suggested Fix Direction

  • In ZIP generation, use symlink_metadata() or DirEntry::file_type() to detect symlinks before following the path.
  • When skip_symlinks is true, skip symlink entries before calling metadata() or File::open().
  • For both ZIP and TAR archive generation, resolve symlink targets and reject or skip any entry whose resolved path is outside the served root.
  • Add regression tests for archive downloads where the served directory contains link_to_outside -> ../outside-secret.txt, including a ZIP test with --no-symlinks.