Jan 24, 2026·6 min read·10 visits
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.
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 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:
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.
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.
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:
miniserve instance with --upload-files.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.
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:
~/.ssh/authorized_keys with their own public key. Result: SSH access as the user running miniserve./etc/cron.d/ or /etc/periodic/. Result: The system executes the attacker's script automatically.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 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:
--upload-files, turn it off. It's the safest way to operate.For Developers:
open with flags that enforce your constraints.O_TMPFILE (on Linux) to write data to an unnamed temporary file first, then linkat it into place. This is atomic and avoids partial file uploads appearing in the directory.rename operations, which are atomic on POSIX filesystems.CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H| 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) |
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.