· 3 min read · 616 words
Bcrypt Stops Reading After 72 Bytes
Bcrypt silently ignores everything past the 72nd byte of a password. Here is where that bites, why pre-hashing with SHA-256 has its own trap, and what to do.
- bcrypt
- limits
- passwords
Bcrypt takes the first 72 bytes of the password and throws the rest away. It does not warn you, it does not error, and the hash it returns looks completely normal.
For an ordinary password this never comes up. Nobody types 72 characters. It starts to matter as soon as something other than a human is producing the input.
Bytes, not characters
The limit is measured in bytes after UTF-8 encoding, which is not the same as the length of the string. A password of plain ASCII gets 72 characters. A password written in Japanese gets 24, because each of those characters takes three bytes. An emoji usually takes four.
The byte counter on the homepage fills up as you type, so you can watch a passphrase cross the line in real time.
Where it actually bites
Passphrases
Password managers and security guides both push people toward long passphrases, and a six word phrase with separators runs to around 40 characters. That is still fine. Somebody following the advice enthusiastically with a twelve word phrase is not.
Pre-hashed input
A common pattern is to hash the password in the client and send the digest, then bcrypt the digest on the server. A SHA-512 digest written as hex is 128 characters, so bcrypt keeps 72 of them and discards 56. You have quietly cut the search space, and two passwords that share their first 72 hex characters now produce the same bcrypt hash.
Concatenated pepper
Appending a secret pepper to the end of the password is the wrong order. If the password is already long, the pepper falls off the end and does nothing at all. Use HMAC with the pepper as the key, or put the pepper first, or better, do not concatenate secrets into password inputs.
What different libraries do about it
| Library | Behaviour past 72 bytes |
|---|---|
| golang.org/x/crypto/bcrypt | Returns an error and refuses to hash |
| Python bcrypt 4.1 and later | Raises ValueError |
| bcryptjs and node bcrypt | Truncates silently |
| PHP password_hash | Truncates silently |
| jBCrypt and Spring Security | Truncates silently |
Go got this right. Failing loudly is the correct behaviour for something the caller almost certainly did not intend.
The SHA-256 pre-hash, and its trap
The standard fix for long inputs is to hash the password with SHA-256 first, then bcrypt the result. The digest is always the same length, so nothing gets truncated.
The trap is the encoding. A raw SHA-256 digest is 32 bytes, which is comfortably under the limit, but those bytes can contain a zero byte. Some bcrypt implementations treat the password as a C string and stop at the first zero, which would leave you hashing a few bytes of digest and nothing else. Base64 the digest first and the problem goes away, at 44 characters and no zero bytes.
import { createHash } from 'node:crypto';
function prepare(password) {
return createHash('sha256').update(password, 'utf8').digest('base64');
}
const hash = await bcrypt.hash(prepare(password), 12);Whatever you choose, apply it in both places. Hashing the prepared value at signup and the raw value at login means nobody can log in, and the bug reads as a mystery until somebody diffs the two code paths.
Or use something without the limit
Argon2id has no 72 byte ceiling and is the current recommendation from OWASP for new applications. If you are starting fresh and have no reason to stay on bcrypt, this whole class of problem is not one you need to have.
If you already run bcrypt on a live database, the limit is not a reason to migrate on its own. Add a maximum length to your signup validation, pick a number under 72, and tell people about it in the form rather than truncating behind their back.