Feb 4, 2026·6 min read·28 visits
A critical flaw in the `openmls` library allowed attackers to bypass cryptographic verification by providing truncated or empty authentication tags. The issue stemmed from Rust's `.zip()` iterator, which silently stops comparing when the shortest input is exhausted. Fixed in v0.7.2.
In the world of cryptographic implementation, 'constant-time' comparison is the gold standard for preventing side-channel attacks. Developers bend over backward to ensure that checking a signature takes exactly the same amount of time, regardless of whether it's correct or not. But in a cruel twist of irony, the developers of `openmls`—a Rust implementation of the Messaging Layer Security (MLS) protocol—focused so hard on the timing that they forgot the length. Due to a quirk in how Rust's iterators handle zipping, the library's equality check would happily accept an empty byte array as a valid cryptographic tag. This allowed attackers to bypass message authentication entirely by simply providing... nothing.
Messaging Layer Security (MLS) is the designated successor to the Double Ratchet protocol (used by Signal, WhatsApp, etc.), designed to bring end-to-end encryption to large groups efficiently. It is complex, math-heavy, and relies entirely on strict cryptographic verification. openmls is a prominent Rust implementation of this standard.
The core of any such system is the ability to verify that a message actually came from who it says it did. This is done via Message Authentication Codes (MACs) or confirmation tags. When a message arrives, the system calculates what the tag should be and compares it to what was received.
Usually, we worry about timing attacks here—where an attacker measures how long the comparison takes to guess the secret key byte-by-byte. The openmls team knew this. They wrote a equal_ct (constant-time equality) function to prevent it. Unfortunately, in their pursuit of side-channel safety, they opened a massive logical door: they stopped checking if the provided tag was actually the right size.
The vulnerability lies in a fundamental behavior of Rust's iterator system, specifically the .zip() method. In many languages, if you try to zip two arrays of different lengths, you might get an error or an exception. In Rust, .zip() is designed to be safe and lazy: it creates an iterator that yields pairs of elements until either of the two underlying iterators is exhausted.
This behavior is documented, standard, and usually desirable—except when you are comparing cryptographic secrets. The vulnerable function, equal_ct, took two byte slices (a and b) and zipped them together to check for differences.
If the attacker controls a (the input tag) and the system controls b (the expected secret tag), the attacker can simply provide an empty slice []. The .zip() iterator sees that a is empty and immediately stops. The loop body never executes. The difference accumulator variable, initialized to 0, remains 0. The function returns true. It's the cryptographic equivalent of a bouncer stepping aside because you didn't hand him an ID card to check.
Let's look at the code that caused the issue. It's elegant, idiomatic Rust, and completely insecure.
The Vulnerable Code:
fn equal_ct(a: &[u8], b: &[u8]) -> bool {
let mut diff = 0u8;
// CRITICAL: .zip() stops at the shortest iterator
for (l, r) in a.iter().zip(b.iter()) {
diff |= l ^ r;
}
diff == 0
}If a is empty, the loop runs 0 times. diff stays 0. 0 == 0 is true. Access granted.
The Fix:
The patch is painfully simple. Before doing the fancy constant-time loop, you must assert that the two things being compared are actually the same size. Since the length of a MAC is usually public knowledge (defined by the cipher suite), this check doesn't leak sensitive data.
fn equal_ct(a: &[u8], b: &[u8]) -> bool {
// The Fix: Explicitly check lengths first
if a.len() != b.len() {
log::error!("Incompatible values");
return false;
}
let mut diff = 0u8;
for (l, r) in a.iter().zip(b.iter()) {
diff |= l ^ r;
}
diff == 0
}This addition ensures the loop always processes the full expected length of bytes, preventing the truncation attack.
Exploiting this does not require a supercomputer or advanced math. It requires a debugger and the audacity to send nothing.
Scenario: You are an attacker sitting on the network (MITM) or a malicious member of a group. You want to inject a fake Commit message into the MLS group, which normally requires a valid ConfirmationTag.
Commit message structure with your malicious content.ConfirmationTag (usually 32 bytes of high-entropy randomness), you provide a 0-byte array (or just an empty vector).openmls client receives the message. It calculates what the tag should be (e.g., 0xDEADBEEF...).equal_ct(received_tag, expected_tag). Since received_tag is empty, the check passes instantly.Alternatively, you could brute-force the first byte. If you send a 1-byte tag, you have a 1 in 256 chance of matching the first byte of the real tag. But why gamble when sending nothing wins 100% of the time?
This is a catastrophe for a cryptographic library. The entire premise of MLS is that membership changes and message streams are cryptographically secured. By bypassing the equal_ct check, an attacker effectively disables authentication.
In a real-world scenario, this could allow:
Add or Remove proposals to kick out legitimate users or add attacker-controlled ghost users.This vulnerability is a stark reminder that "memory safety" (which Rust provides) does not equal "logic safety". The memory was never corrupted; the logic was simply flawed.
The fix was released in openmls v0.7.2. If you are using an older version, your application is wide open.
Remediation Steps:
Cargo.toml.openmls version.cargo update -p openmls to pull the latest version.Cargo.lock that you are on at least 0.7.2.If you cannot upgrade immediately, you must implement a wrapper around calls to openmls that explicitly validates the length of incoming tags before passing them to the library, though this is risky and difficult to cover 100% of code paths.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
openmls OpenMLS | < 0.7.2 | 0.7.2 |
| Attribute | Detail |
|---|---|
| CWE | CWE-1254 (Incorrect Comparison) |
| Attack Vector | Network |
| CVSS | 9.8 (Critical) |
| Impact | Authentication Bypass |
| Language | Rust |
| Fix Commit | 91ec049ffc2fa3766110223aa2aabe0303837af8 |
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.