Feb 21, 2026·6 min read·10 visits
Static Web Server (SWS) < 2.41.0 optimized its Basic Auth too much. It checked usernames first and returned early if they didn't exist. This logic created a timing discrepancy: invalid users returned instantly (or with a specific timing profile), while valid users triggered the slow hashing mechanism (or a different profile). Attackers can stopwatch the server to harvest valid usernames.
A classic timing-based side-channel vulnerability in Static Web Server (SWS) allows remote attackers to enumerate valid usernames. By measuring the microsecond-level differences in response times during Basic Authentication, adversaries can distinguish between 'User Not Found' and 'User Found, Password Wrong' states, effectively bypassing the first layer of authentication defense.
We love Rust. It saves us from double-frees, null pointer dereferences, and the general memory-unsafe chaos of C++. But here's the uncomfortable truth: the borrow checker doesn't care about your logic flow. It won't stop you from designing a beautiful, memory-safe side-channel.
Static Web Server (SWS) is exactly what it sounds like—a high-performance, async web server focused on speed. And speed is exactly what got them into trouble here. In the quest for optimization, the developers committed a cardinal sin of cryptography engineering: they short-circuited an authentication check.
Imagine a bouncer at a club. If you're not on the list, he kicks you out immediately. If you are on the list, he spends 10 seconds checking your ID card under a UV light. If I'm watching from across the street, I don't need to see the list. I just need to watch how fast people get kicked out. If the bouncer tosses them in 0.1 seconds, they aren't on the list. If it takes 10.1 seconds and then they get tossed, they're on the list (but had a fake ID). That's CVE-2026-27480 in a nutshell.
The vulnerability resides in the Basic Auth handling logic. When a user sends an Authorization: Basic <b64> header, the server needs to validate two things: the username and the password.
The developers, thinking logically (but not securely), decided: "Why bother checking the password if the username is wrong? That's a waste of CPU cycles!" This is true in a standard application, but fatal in a security context.
The code implemented an early return. If the username didn't match the config, it returned 401 Unauthorized instantly. If the username did match, it proceeded to call bcrypt_verify.
Here is the catch: bcrypt is designed to be slow. It's a key stretching algorithm meant to burn CPU to stop brute-forcing. By conditionally executing this heavy operation only for valid users, SWS created a massive timing signal. An attacker sends a username. If the response comes back in 0.4ms, it's invalid. If it takes a different amount of time (dominated by the bcrypt cost or lack thereof), it's valid. The server effectively shouts, "I know this guy!" simply by taking its time.
Let's look at the crime scene in src/basic_auth.rs. This is the pre-patch logic that caused the headache.
// The Fatal Short-Circuit
if credentials.0.username() != userid {
// BOOM: Early return.
// The CPU skips the heavy lifting below.
return Err(StatusCode::UNAUTHORIZED);
}
// This only runs if the user exists.
match bcrypt_verify(credentials.0.password(), password) {
Ok(valid) if valid => Ok(()),
Ok(_) => Err(StatusCode::UNAUTHORIZED),
// ...
}The fix, introduced in version 2.41.0, forces the server to do the work regardless of the username validity. It calculates both conditions and combines them at the end. This is a "Constant Time" (or at least "Constant Path") approach.
// The Fix: Run everything, check later.
let user_match = credentials.0.username() == userid;
// Even if the user is wrong, we still verify the password.
// Ideally, we'd verify against a dummy hash to keep timing exact,
// but just running the function closes the massive gap.
let password_match = bcrypt_verify(credentials.0.password(), password)
.inspect_err(|err| tracing::error!("verify error: {:?}", err))
.unwrap_or(false);
// Combine the booleans. No early exit.
let valid = user_match && password_match;
valid.then_some(()).ok_or(StatusCode::UNAUTHORIZED)> [!NOTE]
> While this closes the massive bcrypt timing gap, strict constant-time comparison for the username string itself (==) is still tricky in high-level languages like Rust without using crates like subtle.
Exploiting this isn't as simple as running time curl. Network jitter (latency variance) over the internet is usually higher than the processing time of a local comparison. To exploit this reliably, we need statistics.
An attacker would perform a timing attack using the following methodology:
NON_EXISTENT_USER_999). Calculate the mean response time and standard deviation.admin).admin deviates significantly (beyond 3 standard deviations) from the baseline, you have a hit.In the PoC data provided for this CVE, the difference was small (~0.15ms) but statistically significant. In a real-world scenario where bcrypt is configured with a higher work factor (e.g., cost 10 or 12), the difference would be hundreds of milliseconds—making the attack trivial even over a laggy WiFi connection.
You might ask, "So what if they know my username is admin?"
Knowing the username cuts the brute-force search space in half. Actually, it does more than that. It allows Password Spraying. Instead of trying one password against 10,000 potential usernames (noisy, high failure rate), I can confirm the username contractor_dev exists and then hammer that specific account with the top 500 passwords.
Furthermore, in corporate environments, usernames often follow predictable patterns (e.g., firstname.lastname). This vulnerability allows an attacker to validate an employee list against the external web server, mapping out the organization's internal structure from the outside.
This is a CWE-204: Observable Response Discrepancy. It scores a CVSS 5.3 (Medium), but in the hands of a targeted attacker, it is the key that unlocks the door to further brute-force attacks.
The remediation is straightforward: Update to Static Web Server v2.41.0.
The patch implements a logic flow where the computationally expensive path is always taken. By executing bcrypt_verify regardless of the username match, the server ensures that the response time is dominated by the hashing algorithm for all requests, masking the username check.
If you cannot patch immediately:
fail2ban) can confuse statistical timing attacks by adding artificial delays or blocking the IP before enough data points are gathered.admin, root, or test.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
static-web-server static-web-server | < 2.41.0 | 2.41.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-204 (Observable Response Discrepancy) |
| CVSS v3.1 | 5.3 (Medium) |
| Attack Vector | Network (Remote) |
| Attack Complexity | Low (Statistical) |
| Impact | Confidentiality (User Enumeration) |
| Exploit Status | Proof-of-Concept Available |
Observable Response Discrepancy