Skip to content

· 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

LibraryBehaviour past 72 bytes
golang.org/x/crypto/bcryptReturns an error and refuses to hash
Python bcrypt 4.1 and laterRaises ValueError
bcryptjs and node bcryptTruncates silently
PHP password_hashTruncates silently
jBCrypt and Spring SecurityTruncates 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.

Read next