Jan 23, 2026·5 min read·19 visits
Spring Security tried to fix a password truncation issue (CVE-2025-22228) by banning passwords longer than 72 bytes. However, they enforced this check too early in the authentication flow. By sending a 73+ byte password, attackers could trigger an immediate exception for non-existent users (bypassing the 'dummy' hash), while existing users took longer to process. This timing discrepancy allowed for reliable username enumeration.
A medium-severity timing attack vulnerability in Spring Security's DaoAuthenticationProvider. An improperly implemented length check for BCrypt passwords allowed attackers to bypass timing mitigations and enumerate usernames by analyzing response times.
Spring Security is the gold standard for Java authentication. But in early 2025, the maintainers learned a hard lesson in "fixing things until they break." This is the story of CVE-2025-22234, a vulnerability that proves the road to hell is paved with good intentions—and strict input validation.
It all started when someone realized the BCrypt algorithm has a fundamental limitation: it silently ignores any password characters beyond the 72nd byte. Spring decided to fix this "silent truncation" by throwing an error if a user provided a long password. Sounds reasonable, right? It's better to fail loud than to give a false sense of security.
Wrong. By throwing that error indiscriminately, they accidentally gave attackers a high-precision stopwatch and a map to every valid username in the database. It’s like installing a high-tech burglar alarm that screams "NOBODY IS HOME" the second someone touches the doorknob.
To understand this bug, you have to understand the ancient curse of BCrypt. The algorithm has a hard limit: it only hashes the first 72 bytes of input. Anything after that is ghost data. In a previous patch (CVE-2025-22228), Spring Security decided to enforce this limit strictly.
The logic was added to the BCrypt class: if password.length > 72, throw an IllegalArgumentException. The problem is that authentication happens in two distinct contexts:
The developers applied the exception to both scenarios. This created a logic bomb inside DaoAuthenticationProvider, the component responsible for juggling user lookups and password checks.
Let's look at DaoAuthenticationProvider. It tries to be clever to prevent timing attacks. If a user isn't found in the database, it runs a "dummy" hash operation so the request takes roughly the same amount of time as a successful lookup.
if (user == null) {
// Run a dummy check to waste time and hide the fact the user is missing
mitigateAgainstTimingAttack(authentication);
throw new BadCredentialsException(...);
}Inside mitigateAgainstTimingAttack, it calls the vulnerable BCrypt method. In version 6.4.4, the code looked like this:
private static String hashpw(byte passwordb[], String salt, boolean for_check) {
// THE BUG: This throws immediately, before any CPU work is done
if (passwordb.length > 72) {
throw new IllegalArgumentException("password cannot be more than 72 bytes");
}
// ... expensive hashing loop that takes ~100ms ...
}Do you see it? If I send a 73-byte password for a non-existent user, the "dummy" check crashes instantly due to the exception. It doesn't waste time. It exits.
However, if the user exists, the application loads the user details first, potentially checks the hash differently, or hits the exception later in the pipeline. This creates a massive timing gap.
This vulnerability allows for a classic Side-Channel Timing Attack. We can enumerate users by measuring how fast the server rejects us. Here is the attack chain:
admin and a password string of 75 As.The Results:
DaoAuthenticationProvider sees user == null. It calls mitigateAgainstTimingAttack. The BCrypt class sees 75 bytes and throws the exception immediately. Total time: ~5ms.> [!NOTE] > The difference between 5ms and 150ms is an eternity in network terms. It's trivially easy to distinguish, even with network jitter.
The fix, released in version 6.4.5 (and others), is elegant in its simplicity. The developers realized they needed to distinguish between creating a password and checking one.
The patched code introduces context awareness using the for_check boolean:
private static String hashpw(byte passwordb[], String salt, boolean for_check) {
// FIXED: Only enforce length limit if we are NOT checking (i.e., we are encoding a new password)
if (!for_check && passwordb.length > 72) {
throw new IllegalArgumentException("password cannot be more than 72 bytes");
}
// Proceed with hashing (using first 72 bytes) to maintain timing consistency
// ...
}Now, if I send a long password for a ghost user, the system shrugs, proceeds to the expensive hashing loop (using the first 72 bytes), and returns in 150ms. The timing leak is plugged because the "dummy" calculation actually runs, ensuring User Found and User Not Found take the same amount of time.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Spring Security VMware | 6.4.4 | 6.4.5 |
Spring Security VMware | 6.3.8 | 6.3.9 |
Spring Security VMware | 5.8.18 | 5.8.19 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-208 |
| Attack Vector | Network |
| CVSS | 5.3 (Medium) |
| Impact | Information Disclosure (Username Enumeration) |
| Exploit Status | PoC Available |
| Root Cause | Exception thrown before constant-time operation |
The application functions with a timing discrepancy, where the time taken to respond to a query varies based on the input, allowing an attacker to infer information.