Skip to content

· 5 min read · 1,093 words

Bcrypt Stops Reading After 72 Bytes

Bcrypt never reads past the 72nd byte of a password, and most libraries discard the rest in silence. 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 ignores the rest. In most libraries that happens in silence: no warning, no error, and a hash that looks completely normal. A few now refuse the input instead, which is the better behaviour and is covered further down.

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 ErrPasswordTooLong and refuses to hash
Python bcrypt 5.0 and laterRaises ValueError
Python bcrypt before 5.0Truncates silently
bcryptjs and node bcryptTruncates silently
PHP password_hashTruncates silently
jBCrypt and Spring SecurityTruncates silently

Go got this right and Python came round to it. Failing loudly is the correct behaviour for something the caller almost certainly did not intend. Check which side of the line your own version sits on before you rely on either behaviour, because the Python change landed in a major release and silent truncation was the documented behaviour for years before it.

The pre-hash, and its two traps

The usual suggestion 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. It works, and there are two things to get right before you ship it.

Trap one: zero bytes

A raw SHA-256 digest is 32 bytes, comfortably under the limit, but those bytes can contain a zero. 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, with no zero bytes and a length that still fits.

Trap two: password shucking

This one is easier to miss because the code looks correct. If you store bcrypt of a plain unkeyed hash, an attacker who has a corpus of that same hash from other breaches can test each entry from the corpus against your bcrypt hash directly. That is one bcrypt operation per corpus entry rather than per password guess, and when one matches they hold the inner digest and can attack it at whatever speed the fast hash runs at. The bcrypt layer gets peeled off, which is where the name comes from.

The consequence is that plain SHA-256 in front of bcrypt is worth roughly what SHA-256 alone is worth against an attacker who has the right corpus. OWASP says this about SHA-512 in the same words, and it is why the cheat sheet does not recommend a bare pre-hash.

What to do instead

Use HMAC with a secret pepper as the pre-hash rather than a bare digest. The inner value is then something an attacker cannot compute or look up in any corpus, because it depends on a key that is not in the database. This is the construction OWASP names, as bcrypt of base64 of HMAC-SHA384 keyed with the pepper:

import { createHmac } from 'node:crypto';

// Keyed, so no corpus of plain digests helps an attacker.
// Base64, so no zero byte can cut the input short.
function prepare(password) {
  return createHmac('sha384', process.env.PASSWORD_PEPPER)
    .update(password, 'utf8')
    .digest('base64');
}

const hash = await bcrypt.hash(prepare(password), 12);
const ok = await bcrypt.compare(prepare(candidate), user.passwordHash);

The pepper has to live somewhere the database dump does not reach, and rotating it is the awkward part. The write up on peppering covers both. If you cannot run a pepper at all, a maximum length in the signup form is the more honest option of the two, and the next section says why.

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 say the limit out loud

The option that needs no cryptography at all is a maximum length on the signup form. Pick a number under 72, put it in the field and in the error message, and you have removed the problem without introducing a pepper to manage. It is unfashionable and it is honest. What you must not do is accept 200 characters, keep 72, and say nothing.

Or use something without the limit

Argon2id has no 72 byte ceiling, takes long passphrases as they are, and is the first recommendation in the current OWASP guidance. That guidance goes further and treats bcrypt as a legacy choice, to be used where Argon2 and scrypt are not available. If you are starting fresh, this whole class of problem is one you can decline to have.

On a live bcrypt database the calculation is different. The 72 byte limit on its own is a weak reason to change function, because a length cap or a keyed pre-hash both close it for far less work than a migration. That is a judgement about effort rather than a reading of the standard, and the comparison of the four functions sets out both sides.

Read next