On October 30, 2024, Okta published a security advisory with an oddly specific trigger: in AD/LDAP Delegated Authentication, a user whose username was 52 characters or longer could, under certain conditions, log in without presenting the correct password. The bug had been live since July 23, 2024.
The root cause wasn't exotic. It was a property of bcrypt that has been documented since 1999 and that most engineers have never internalized: bcrypt stops reading its input at 72 bytes and silently ignores everything after that.
Where the 72 comes from
bcrypt is built on Blowfish — specifically on eksblowfish, a variant with a deliberately expensive key schedule, described by Niels Provos and David Mazières in their 1999 USENIX paper. Blowfish derives 18 32-bit subkeys, P1 through P18, collectively called the P-array, from the encryption key. The paper describes how the key enters the cipher:
The first 32 bits of the key are XORed with P1, the next 32 bits with P2, and so on. The key is viewed as being cyclic; when the process reaches the end of the key, it starts reusing bits from the beginning to XOR with subkeys.
That's the whole story of how your password gets into bcrypt: it is XORed, 4 bytes at a time, into 18 subkeys. 18 subkeys × 4 bytes = 72 bytes. Byte 73 of your input has no subkey left to land in. There is no error path, because nothing fails — the key schedule simply never consumes those bytes.
Two details of that quote deserve more attention than they get:
- Truncation is structural, not a bounds check someone forgot. The algorithm has exactly 72 bytes of key material capacity. Classic implementations don't even measure your input against a limit; they walk the P-array and stop.
- Short keys wrap around. A password shorter than 72 bytes is treated as cyclic and reused from the beginning until all 18 subkeys are filled. Your 8-character password is XORed into the P-array nine times.
Amusingly, the paper itself never says 72. It specifies the key as "a user-chosen password of up to 56 bytes (including a terminating zero byte when the key is an ASCII string)," and later, comparing against MD5 crypt, states that "bcrypt has a maximum of 55 bytes." The 72-byte figure is the de facto limit of the OpenBSD implementation everyone actually copied — bcrypt(3) on OpenBSD documents it in one flat sentence: "The maximum password length is 72."
Anatomy of the Okta bypass
Okta's advisory is unusually direct about the mechanism: for AD/LDAP Delegated Authentication, "the Bcrypt algorithm was used to generate the cache key where we hash a combined string of userId + username + password."
That cache exists so users can still sign in when the AD/LDAP agent is unreachable. Concatenate the three fields and the failure mode writes itself: if userId + username alone reaches 72 bytes, the password contributes zero bytes to the hash. Every password produces the same cache key. Whoever presents that username — with any password at all — matches the stored entry from the last successful login. Okta's observed threshold for this was a username of 52 characters or more.
Exploitation required a specific alignment of conditions, all listed in the advisory: delegated auth in use, no MFA, a username of 52+ characters, an existing cache entry from a prior successful login, and the cache being consulted because the agent was down or unreachable. The fix, shipped the same day the bug was found internally: stop using bcrypt for cache keys and switch to PBKDF2.
The deeper lesson isn't "Okta made a mistake." It's that bcrypt is a password-verification function, not a general-purpose KDF, and it fails its misuse cases silently. A 72-byte cap is a non-issue when the input is a human-typed password. It becomes an authentication bypass the moment you feed it a machine-built composite string whose length an attacker partially controls — and a username is exactly that.
Your language disagrees with itself about what to do
Because truncation lives in the algorithm, every binding has to choose how to surface it. The ecosystem has quietly split into two camps — truncate silently, or refuse loudly:
| Implementation | Behavior past 72 bytes |
|---|---|
OpenBSD bcrypt(3) | Truncates; man page states the 72 maximum |
PHP password_hash() with PASSWORD_BCRYPT | Truncates; the manual carries an explicit caution |
Go golang.org/x/crypto/bcrypt | Refuses: GenerateFromPassword returns ErrPasswordTooLong for inputs over 72 bytes |
Python pyca/bcrypt | Truncated silently for years; since 5.0.0, hashpw raises ValueError |
PHP's manual states it plainly: using PASSWORD_BCRYPT "will result in the password parameter being truncated to a maximum length of 72 bytes." Which means this verifies successfully:
$hash = password_hash(str_repeat("A", 72) . "this part never existed", PASSWORD_BCRYPT);
password_verify(str_repeat("A", 72) . "something else entirely", $hash); // trueGo took the opposite stance from the start — its docs call 72 bytes "the longest password bcrypt will operate on" and hand back an explicit error. And pyca/bcrypt's 5.0.0 changelog records the ecosystem's slow change of heart: "Passing hashpw a password longer than 72 bytes now raises a ValueError. Previously the password was silently truncated, following the behavior of the original OpenBSD bcrypt implementation."
If your test suite never exercises a 73-byte password, a library upgrade can flip you from one camp to the other without a deprecation period. That pyca change turned silently-wrong into loudly-broken — the right direction, but only if you find out in CI rather than in production signups.
What to actually do
- Never feed bcrypt a composite string. Cache keys, session tokens,
userId + username + password— that's HMAC or HKDF/PBKDF2 territory. Okta's own remediation was exactly this substitution. - Handle the limit explicitly at the boundary. Either reject passwords over 72 bytes at signup (measured in bytes — multibyte UTF-8 hits the cap well before 72 characters), or prehash.
- If you prehash, encode the digest. The standard construction, straight from pyca's README, is
bcrypt(base64(sha256(password))). The base64 step is not decoration: the paper's key format assumes a terminating zero byte for ASCII keys, and a raw SHA-256 digest can contain zero bytes anywhere. Base64 keeps the input NUL-free and well under 72 bytes. - Pin your library's truncation behavior in a test. One assertion — "73-byte input either verifies correctly or throws" — catches both silent truncation and a semantics change arriving in a version bump.
bcrypt remains a fine password hash a quarter-century on; cost-parameterized design aged remarkably well. But the 72-byte cliff is the sharpest example of a general rule: cryptographic primitives don't fail on misuse, they just quietly compute the wrong thing. The advisory that proves it has a date in 2024, not 1999.
