skip to content

In PHP, what is bcrypt's 72-byte limit in password_hash(), and how does it bite long or multibyte passwords?

level: middleimportance: should knowfreq 30%

answer

  1. bcrypt truncates input at 72 bytes
  2. bytes, not characters — multibyte counts double
  3. silent truncation, no error
  4. two long passwords can collide
  5. ARGON2ID has no such limit

basics

~20 s

With PASSWORD_BCRYPT, password_hash() silently truncates the password to 72 bytes, so anything past byte 72 is ignored. Because it counts bytes, multibyte UTF-8 characters use several each. Two long passwords sharing a 72-byte prefix verify as equal. ARGON2ID has no such limit.

solid answer

~50 s

bcrypt operates on at most **72 bytes** of input, and `password_hash($pw, PASSWORD_BCRYPT)` **silently truncates** anything longer — there is no warning or error. The limit is in **bytes, not characters**, so a passphrase of multibyte UTF-8 characters (each emoji or accented letter is 2–4 bytes) can hit 72 bytes well before 72 characters. Two consequences: a very long password is only as strong as its first 72 bytes, and **two distinct passwords that share the same first 72 bytes will `password_verify` as identical**. This is a property of bcrypt itself, not a PHP bug, but it surprises people who set a high maximum length. Mitigations: use `PASSWORD_ARGON2ID`, which has **no 72-byte limit**; or, if you must stay on bcrypt, pre-hash into a fixed-size printable value first (for example Base64-encode an HMAC-SHA-256 of the password) so the bcrypt input is short and null-byte-free. Do not simply reject passwords over 72 characters — that both annoys users and misdescribes the limit, which is in bytes.

go deeper

for a junior

Know that bcrypt only uses the first 72 bytes and that Argon2id does not have this limit.

for a middle

Explain that truncation is silent and counted in bytes, so multibyte passwords hit it sooner, and that shared 72-byte prefixes collide.

for a senior

Diagnose a 'wrong password still works' report as truncation and fix it with Argon2id or a pre-hash to a short value.

for a principal

Decide the org policy on maximum length and pre-hashing, balancing user experience against the bcrypt constraint.

## The limit, precisely Bcrypt's design processes only the first **72 bytes** of the password. In PHP, `password_hash($password, PASSWORD_BCRYPT)` (and therefore `PASSWORD_DEFAULT` while it resolves to bcrypt) reflects that: the manual states the password "being truncated to a maximum length of 72 bytes." The truncation is **silent** — you get a normal-looking hash back, and `password_verify` will later accept any password whose first 72 bytes match. ## Why "bytes, not characters" matters PHP strings are byte strings. UTF-8 encodes: - ASCII (`a`, `1`, `!`) as **1 byte**, - most accented Latin and Greek/Cyrillic letters as **2 bytes**, - most CJK characters as **3 bytes**, - emoji and many symbols as **4 bytes**. So a "passphrase" like a string of emoji reaches the 72-byte ceiling in as few as ~18 characters. A user who thinks they typed a 40-character password may have supplied only its first 30 or so characters to bcrypt. ## The two failure modes | Symptom | Cause | |---|---| | A long password "works" even when the user mistypes the tail | Everything past byte 72 was truncated before hashing | | Two different long passwords log into the same account | They share the same first 72 bytes | Neither is exploitable in most systems (an attacker still needs the first 72 bytes), but both violate the intuition that the whole password is checked, and the second is a real correctness bug for users with long secrets or password-manager output. ## What to do about it 1. **Prefer `PASSWORD_ARGON2ID`.** Argon2 has no 72-byte input limit and is memory-hard. If your PHP build includes Argon2 support, this removes the problem outright. 2. **If bcrypt is required, pre-hash to a fixed short value.** A common pattern is to compute a keyed digest and Base64-encode it so the bcrypt input is short and contains no NUL byte: ```php $pre = base64_encode(hash_hmac('sha256', $password, $pepperKey, true)); $hash = password_hash($pre, PASSWORD_BCRYPT); ``` Verification pre-hashes the same way before `password_verify`. (This also folds in a server-side pepper; the pepper *design* is a concept-home topic — here the point is only that the bcrypt input becomes short and safe.) 3. **Do not cap length at 72 characters.** The limit is in bytes, so a byte-aware check would be `strlen($pw) <= 72`, but rejecting long passwords is user-hostile and unnecessary if you use Argon2id or pre-hashing. Allow long passwords and handle the limit correctly instead. ## A note on NUL bytes Historically, a NUL byte (``) inside a password could truncate bcrypt input early; pre-hashing to Base64 avoids embedding NULs. This is another reason the pre-hash pattern uses a printable encoding rather than raw binary from `hash_hmac(..., true)`. The choice between bcrypt and Argon2 as *algorithms* is a security-design question; the PHP-specific facts here are the silent 72-byte (not 72-character) truncation, that Argon2id avoids it, and the pre-hash workaround.

  • Would capping the input at 72 *characters* fix the problem?
    Not correctly. The limit is 72 *bytes*, so 72 multibyte characters can be far more than 72 bytes, and you would still truncate. A byte check (`strlen($pw) <= 72`) is accurate, but rejecting long passwords is unnecessary — use Argon2id or pre-hash instead so you can accept passwords of any length.
  • Does the 72-byte limit apply to `PASSWORD_ARGON2ID`?
    No. The 72-byte ceiling is specific to bcrypt's construction. `PASSWORD_ARGON2ID` (and `PASSWORD_ARGON2I`) process the full input, so switching to Argon2id removes the truncation and its collision surprise entirely, provided your PHP build has Argon2 support.
  • Why Base64-encode the pre-hash instead of using raw HMAC bytes?
    Raw binary can contain a NUL byte, which historically truncates bcrypt input early, and it isn't a clean string to pass around. Base64 (or hex) yields a short, printable, NUL-free value that safely fits under 72 bytes, so the bcrypt input is well-defined.

saying these in an interview costs you the question

  • bcrypt truncates at 72 characters, so multibyte is fine
  • password_hash raises an error when the password is too long
  • the 72-byte limit also applies to Argon2id
  • a 100-character password is fully checked by bcrypt
  • you must reject any password longer than 72 characters