The 73rd Byte: How a Spring Security Fix Created a Timing Leak
Jan 23, 2026·5 min read·7 visits
Executive Summary (TL;DR)
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.
The Hook: The Law of Unintended Consequences
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.
The Flaw: The 72-Byte Curse
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:
- Registration (Encoding): We definitely want to stop users from creating passwords that will be truncated.
- Login (Matching): We are just checking if the input matches the stored hash.
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.
The Code: The Smoking Gun
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.
The Exploit: Weaponizing the Stopwatch
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:
- Identify Target: Find a Spring Security login form.
- Establish Baseline: Send a normal login request. Note the average response time (e.g., 150ms).
- The Probe: Send a login request with the username
adminand a password string of 75As.
The Results:
- Result A (User NOT Found): The
DaoAuthenticationProviderseesuser == null. It callsmitigateAgainstTimingAttack. TheBCryptclass sees 75 bytes and throws the exception immediately. Total time: ~5ms. - Result B (User FOUND): The provider loads the user from the DB. It proceeds to check the password. Even if it eventually fails, the DB lookup and object hydration take time. Total time: ~150ms.
[!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: Context is King
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.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:NAffected Systems
Affected Versions Detail
| 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 |
MITRE ATT&CK Mapping
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.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.