Feb 12, 2026·6 min read·173 visits
Directus implemented a 'stall' mechanism to hide whether a user exists during password resets. However, they validated the 'reset_url' parameter *after* the user lookup but *before* the stall for existing users. This created a 500ms timing discrepancy: existing users return an error immediately (fast), while non-existing users trigger the artificial delay (slow).
A logic error in the Directus password reset flow allows attackers to enumerate valid email addresses by measuring server response times. By manipulating the 'reset_url' parameter, attackers can bypass the application's anti-enumeration timing protections.
There is a special place in hell reserved for timing attacks. They are subtle, statistical, and often born from the very code intended to prevent them. CVE-2026-26185 is a classic example of this irony, affecting Directus, the popular headless CMS.
Directus developers, being security-conscious folks, knew that password reset forms are a goldmine for attackers. If you type in an email and the server says "Email sent!" versus "User not found," you've just handed the attacker a valid username. To combat this, they implemented a stall() function—a utility designed to force all requests to take a standardized amount of time (500ms), masking the database lookup speed.
But here's the catch: implementing a constant-time response is like trying to carry water in a sieve. You have to plug every hole. In this case, they missed one logic branch, turning their shield into a magnifying glass for attackers.
The vulnerability lies in the UsersService within the @directus/api package. Specifically, the requestPasswordReset method. The goal of this function is simple: look up a user by email and send them a reset link. If the user doesn't exist, the system waits out the stall timer and throws a generic error. If the user does exist, it proceeds to validate parameters.
The flaw was purely chronological. The code performed the expensive operation (database lookup) before validating the input reset_url.
Here is the logic flow in the vulnerable version:
email exists.Forbidden.reset_url is in the allow-list. If not, throw InvalidPayload immediately.Do you see the problem? If I provide a valid email but a garbage URL, the code skips the stall and errors out instantly. If I provide an invalid email and a garbage URL, the code enters the If User Missing block and sleeps for half a second. That 500ms delta is the sound of the server leaking data.
Let's look at the TypeScript source. This is the diff from commit e69aa7a5248c6e3e822cb1ac354dee295df90b2a. It perfectly illustrates how a simple swap of lines closes the side channel.
Vulnerable Code (Before):
// 1. Start the clock
const timeStart = performance.now();
// 2. Look for the user (The leakage source)
const user = await this.getUserByEmail(email);
// 3. If no user, STALL then throw
if (user?.status !== 'active') {
await stall(STALL_TIME, timeStart);
throw new ForbiddenError();
}
// 4. If user exists, check URL.
// OOPS: This throws immediately, bypassing the stall logic above.
if (url && isUrlAllowed(url, ...) === false) {
throw new InvalidPayloadError(...);
}Fixed Code (After):
const timeStart = performance.now();
// 1. Check the URL FIRST.
// If it's bad, fail fast for EVERYONE.
if (url && isUrlAllowed(url, ...) === false) {
throw new InvalidPayloadError(...);
}
// 2. Now look for the user
const user = await this.getUserByEmail(email);
// 3. Standardize timing
if (user?.status !== 'active') {
await stall(STALL_TIME, timeStart);
throw new ForbiddenError();
}By moving the URL validation to the top, the application ensures that a bad reset_url results in a fast rejection regardless of whether the email is in the database. The side channel is closed because the response path is now identical for both valid and invalid users when the URL is malicious.
Exploiting this does not require complex memory corruption or packet injection. It requires a stopwatch. An attacker can write a simple Python script to enumerate users.
The Attack Chain:
/admin endpoints)./auth/password/request.
email: The target email you want to verify (e.g., admin@target.com).reset_url: A known blocked URL or just http://evil.com (assuming the server enforces an allow-list).The Heuristic:
stall() penalty box for half a second.This isn't theoretical. With a 500ms difference, you don't even need statistical averaging. A single request is often enough to confirm existence.
Is this going to bring down the banking system? No. But in the reconnaissance phase of an attack, user enumeration is incredibly valuable.
Knowing valid usernames allows an attacker to:
j.doe@company.com is a valid admin, I can craft a specific spear-phishing email targeting them.For a platform like Directus, which often manages critical data for apps and websites, exposing the list of administrators is a significant breach of privacy.
The fix is straightforward: Update to Directus v11.14.1 or @directus/api v32.2.0.
If you cannot update immediately, you are in a tight spot. You could technically try to ensure your PASSWORD_RESET_URL_ALLOW_LIST is empty or extremely permissive to prevent the validation error from triggering, but that introduces other security risks (like Host Header injection or open redirects).
The only real cure is the patch. This vulnerability serves as a reminder for developers: Input validation should always happen before resource-intensive or sensitive operations. Validate your inputs at the door, not after you've already invited them into the living room.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Directus Directus | < 11.14.1 | 11.14.1 |
@directus/api Directus | < 32.2.0 | 32.2.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-203 (Observable Discrepancy) |
| Attack Vector | Network |
| CVSS | 5.3 (Medium) |
| Impact | Information Disclosure (User Enumeration) |
| Exploit Status | Proof of Concept (Trivial) |
| Affected Component | UsersService.requestPasswordReset |
The product behaves differently or sends different responses depending on whether a user account exists, which allows an attacker to enumerate valid usernames.
An uncontrolled resource consumption vulnerability exists in the Scala-based http4s-blaze-server package of the http4s/blaze library. The vulnerability allows remote, unauthenticated attackers to cause an Out of Memory Error (OOM) and JVM crash by streaming a continuous sequence of small or empty WebSocket continuation frames with the FIN bit set to 0. This bypasses typical payload size checks because of the JVM's per-object allocation overhead, leading to rapid heap exhaustion with minimal network bandwidth.
A critical path traversal vulnerability has been identified in the OpenList Go-based backend package. The vulnerability exists within the batch rename handler because the application does not validate the source filename parameter before constructing filesystems paths. This omission allows authenticated users to escape their designated directory and rename files in sibling paths.
OpenList version 4.2.3 and prior is vulnerable to an authorization bypass and metadata leakage. When configured with the Bleve search engine backend, OpenList fails to perform separator-aware path matching when validating tenant containment. This allows authenticated users to access sibling directories sharing similar name prefixes. Furthermore, the search backend returns unfiltered global result counts, leaking existence verification data of unauthorized files via side-channel analysis.
An authorization bypass vulnerability in OpenList version 4.2.3 and below allows authenticated users to read arbitrary files outside of their designated base directories due to an insecure path prefix check using Go's standard strings.HasPrefix function.
A security policy bypass vulnerability exists in the AWS API MCP Server (awslabs-aws-api-mcp-server) from version 0.2.13 through 1.3.46. When the server fails to load the read-only operations index during startup (due to transient network failures, file permission issues, or other exceptions), it logs a warning but continues running in an insecure, degraded state. Under this condition, the security policy engine fails open, silently skipping all subsequent security checks and consent prompts for the lifetime of the process. This permits unauthorized mutating AWS CLI commands to execute via indirect prompt injection attacks.
An incomplete escaping vulnerability in the npm package 'shescape' allows unauthenticated users to trigger dynamic shell expansions, absolute path disclosure, and command block break-outs on Unix and Windows systems.