Served Cold: Race Conditions and Arbitrary File Overwrite in miniserve
Jan 24, 2026·6 min read·4 visits
Executive Summary (TL;DR)
If you're using `miniserve` with file uploads enabled, you might be serving up your own system files on a silver platter. CVE-2025-67124 is a race condition that lets an attacker trick the server into following a symbolic link during the upload finalization process. By winning the race, they can overwrite files outside the upload directory (like `/etc/shadow` or `~/.ssh/authorized_keys`), leading to Denial of Service or potential Remote Code Execution. The fix? Upgrade immediately or turn off uploads.
A classic Time-of-Check to Time-of-Use (TOCTOU) vulnerability in miniserve v0.32.0 allows attackers to overwrite arbitrary files via symbolic link racing during file uploads.
The Hook: When "Mini" Becomes Major
miniserve is one of those tools that developers love. It's a statically compiled, zero-dependency, "just works" HTTP server written in Rust. Need to share a directory? miniserve .. Done. It's the Swiss Army knife of quick-and-dirty file sharing.
But things get complicated when you stop just serving files and start accepting them. The --upload-files flag transforms this simple file server into a writable drop box. And as any seasoned security researcher knows, allowing users to write to the disk is like inviting a vampire into your house—you have to set very specific ground rules, or you're going to have a bad time.
In version 0.32.0, those ground rules were strictly enforced... but they were enforced with a stopwatch that was just a little too slow. We're looking at a classic concurrency bug in a modern language: a Time-of-Check to Time-of-Use (TOCTOU) vulnerability that proves even Rust's borrow checker can't save you from logic errors.
The Flaw: A Gap in the Armor
The vulnerability lies in how miniserve handles the finalization of uploaded files. When you upload a file, the server needs to decide where to put it and ensure it's not overwriting something it shouldn't—or at least, that was the intention.
The logic follows a familiar, fatal pattern:
- Check: Does the destination path look safe? Is it free?
- Act: Okay, create the file and write the data.
This is the "Time-of-Check" and the "Time-of-Use". In a single-threaded world, this is fine. But filesystems are shared state. Between step 1 and step 2, there is a microsecond-sized window of opportunity. It's like checking if the bridge is down, looking away to tie your shoe, and then driving the car forward assuming the bridge is still there.
An attacker with write access to the upload directory (common in shared hosting or container volumes) can exploit this gap. They wait for the server to validate the filename payload.txt. Right after the check passes, but before the server opens the file descriptor for writing, the attacker swaps payload.txt with a symbolic link pointing to /etc/passwd. The server, blind to the switch, happily opens the link and overwrites the system password file.
The Code: Rust vs. The Filesystem
While Rust provides memory safety, it doesn't automatically enforce atomic filesystem operations. The vulnerable code likely looked something like this (pseudocode representation of the logic flaw):
// The Vulnerable Pattern
let dest_path = upload_dir.join(filename);
// 1. The Check
if !dest_path.exists() {
// <--- ATTACKER SWAPS FILE FOR SYMLINK HERE
// 2. The Use
let mut file = File::create(dest_path)?;
file.write_all(&data)?;
}The issue is that path.exists() and File::create(path) are two separate system calls. The operating system kernel schedules them independently. If the attacker is fast enough (or the server is slow enough), the state of the filesystem changes between those two lines.
The Fix usually involves using low-level file flags to ensure atomicity. In POSIX systems, this means using O_CREAT | O_EXCL. This flag tells the kernel: "Create this file, but fail instantly if it already exists." This combines the Check and the Use into a single atomic instruction, leaving no gap for the attacker to slip in a symlink.
The Exploit: Winning the Race
Exploiting a race condition is usually about brute force and timing. We need to continuously toggle a file between being a "safe" file and a "malicious" symlink, hoping the server catches the symlink at the exact wrong moment.
The Setup:
- You need a
miniserveinstance with--upload-files. - You need write access to the upload directory (e.g., you are a low-privileged user on a shared server, or you have script execution capabilities inside the container).
The Attack Loop:
# The target we want to overwrite
TARGET="/root/.ssh/authorized_keys"
# The bait file
BAIT="payload.txt"
while true; do
# State A: Normal file (passes the check)
touch $BAIT
# State B: Symlink to target (hijacks the write)
rm $BAIT
ln -s $TARGET $BAIT
doneWhile this loop runs, the attacker spams upload requests to miniserve. Most requests will fail (either the file exists, or the write fails permissions). But eventually, the stars align: miniserve checks the path when it's State A (safe), context switches, the loop swaps it to State B (symlink), and then miniserve writes to the target. Bingo. You've just overwritten the root SSH keys with your uploaded content.
The Impact: From File Write to RCE
Why should you panic? Because arbitrary file overwrites are rarely just about defacement. They are the gateway to Remote Code Execution (RCE).
If an attacker can overwrite specific files, they own the system:
- Authorized Keys: Overwrite
~/.ssh/authorized_keyswith their own public key. Result: SSH access as the user runningminiserve. - Cron Jobs: Overwrite a script in
/etc/cron.d/or/etc/periodic/. Result: The system executes the attacker's script automatically. - Config Files: Overwrite
miniserve's own config or binary (if permissions allow). Result: Persistent backdoor.
Even in a restricted container, overwriting /etc/shadow or /etc/passwd can cause a Denial of Service or allow privilege escalation if the container is running as root (which, let's be honest, half the containers on the internet are).
The Fix: Closing the Window
The mitigation is straightforward but requires code changes. You cannot "config" your way out of a race condition in the binary logic, other than disabling the affected feature.
For Users:
- Update: Move to a version later than 0.32.0 immediately.
- Disable Uploads: If you don't absolutely need
--upload-files, turn it off. It's the safest way to operate. - Permissions: Ensure the upload directory is not world-writable or writable by untrusted local users.
For Developers:
- Never check and then open. Always
openwith flags that enforce your constraints. - Use
O_TMPFILE(on Linux) to write data to an unnamed temporary file first, thenlinkatit into place. This is atomic and avoids partial file uploads appearing in the directory. - If you must support overwrites, use secure temporary directories and
renameoperations, which are atomic on POSIX filesystems.
Official Patches
Technical Appendix
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
miniserve svenstaro | = 0.32.0 | 0.33.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-367 (TOCTOU) |
| Attack Vector | Local / Network (Uploads) |
| CVSS | 6.8 (Medium) |
| Impact | Arbitrary File Overwrite |
| Exploit Status | PoC Available |
| Architecture | x86, ARM, etc. (Rust generic) |
MITRE ATT&CK Mapping
The software checks the state of a resource before using it, but the resource's state can change between the check and the use in a way that invalidates the results of the check.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.