miniserve ZIP archive download follows symlink target outside served root even with `--no-symlinks`
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.rslines163-165:--no-symlinksis documented as "Hide symlinks in listing and prevent them from being followed".src/listing.rslines424-428: the archive-download handler passesconf.no_symlinksintoarchive_method.create_archive(...).src/archive.rslines237-249: ZIP generation callsstd::fs::metadata(entry_path), which follows symlinks, and then checksentry_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.rslines274-289: the sameentry_pathis opened withFile::open(&entry_path)and written into the ZIP as a regular file entry, so the outside target's contents are packaged.src/archive.rslines148-155: the TAR path also follows symlinks by default throughtar_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:
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_outsideExpected 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-SECRETI 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()orDirEntry::file_type()to detect symlinks before following the path. - When
skip_symlinksis true, skip symlink entries before callingmetadata()orFile::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.
Source: svenstaro/miniserve