Error bcrypt de 72 bytes: por qué también fallan las contraseñas cortas
Dos problemas distintos producen el mismo mensaje, y solo uno de ellos tiene algo que ver con tu contraseña.
Si tu contraseña de verdad supera el límite de 72 bytes de bcrypt, bcrypt lee los primeros 72 bytes y descarta el resto. Aplicamos hash a dos contraseñas de 82 bytes que compartían sus primeros 72 bytes, bajo un salt fijo. Ambas produjeron $2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S, y bcrypt.compareSync(p2, hash(p1)) devolvió true. La segunda contraseña entra a la cuenta de la primera.
Si tu contraseña es a todas luces corta y aun así recibes esto:
password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
(«la contraseña no puede superar los 72 bytes, trúncala manualmente si hace falta»), entonces el mensaje se equivoca sobre la causa. Con passlib 1.7.4 y bcrypt 5.0.0, una contraseña de 14 bytes lo dispara.
El culpable ahí es una sonda de autodiagnóstico fija de 255 bytes que vive dentro de passlib. Se ejecuta una sola vez, cuando el backend se inicializa, antes de que tu contraseña llegue siquiera a la llamada de hashing. bcrypt 5.0.0 rechaza la sonda, la excepción se escapa y tú acabas leyendo una queja sobre una contraseña que nadie escribió.
El monkey patch sobre __about__ que domina los resultados de búsqueda para este error tampoco lo arregla. Lo volvimos a ejecutar en un proceso limpio, con el parche aplicado antes de import passlib, y el ValueError reapareció sin cambios.
Triaje en 30 segundos: ¿cuál es tu caso?
| Tu contraseña | Cuándo salta el error | Causa raíz | Ve a |
|---|---|---|---|
| Más larga que 72 bytes | Al llamar a hash | Es genuinamente demasiado larga. bcrypt 5.0 lanza error, bcrypt 4.x trunca en silencio | Secciones 2 y 3 |
| Menor de 72 bytes, usando passlib | En la primera llamada del proceso | La sonda de 255 bytes de passlib. No tiene nada que ver con tu contraseña | Sección 4 |
| Contiene chino, japonés o emoji | Parece corta, no lo es | Los caracteres no son bytes | Sección 3 |
| Empezó a fallar tras actualizar una dependencia | Después del despliegue | El cambio incompatible de bcrypt 5.0 | Secciones 4 y 5 |
Si estás en la fila 2, salta directo. Nada de las dos secciones siguientes te va a servir, y la solución es otra.
Qué le hace a tu contraseña el límite de 72 bytes de bcrypt
Por qué bcrypt se detiene en 72
bcrypt está construido sobre Blowfish, y le entrega tu contraseña como clave de Blowfish. Blowfish expande su clave en un arreglo P de 18 subclaves, cada una de 32 bits. Eso da 18 × 4 = 72 bytes de material de clave, y el bucle de expansión vuelve al inicio de la clave una vez que ha llenado las 18 casillas.
Así que el techo es estructural. No es una implementación perezosa ni un búfer configurable que alguien olvidó ampliar. Toda implementación conforme de bcrypt, en cualquier plataforma, tiene el mismo límite, y por eso ves el número 72 tanto en Python como en Node, Go, Java y PHP.
Dos contraseñas distintas, un mismo hash
El truncamiento de contraseñas de bcrypt tiene consecuencias de seguridad concretas, y son fáciles de medir.
Con bcryptjs 3.0.3 y el salt fijo $2a$10$abcdefghijklmnopqrstuv, aplicamos hash a dos contraseñas de 82 bytes cada una:
| Contraseña | Valor | Bytes |
|---|---|---|
| p1 | "A"×72 + "XXXXXXXXXX" | 82 |
| p2 | "A"×72 + "ZZZZZZZZZZ" | 82 |
Ambas produjeron el mismo digest:
$2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S
Dos contraseñas distintas, un mismo hash: true. Y la consecuencia que se sigue de ahí:
bcrypt.compareSync(p2, hash(p1)) // true
Un atacante que conozca los primeros 72 bytes de una frase de contraseña larga puede añadir lo que se le antoje y autenticarse igual. Cada byte más allá de la frontera aporta exactamente cero a la fuerza del hash almacenado, por más cuidado que hayan puesto tus usuarios al elegirlo. Si quieres comprobar un hash que ya tienes contra una contraseña candidata sin montar un script, puedes generar y verificar hashes bcrypt en el navegador y ver tú mismo el mismo comportamiento.
Dónde cae exactamente el límite
Acotamos el corte byte a byte, manteniendo un prefijo compartido y cambiando exactamente un byte después de él:
| Bytes de prefijo idénticos | El byte N+1 difiere en | ¿Mismo hash? |
|---|---|---|
| 70 | 71 | false |
| 71 | 72 | false |
| 72 | 73 | true |
| 73 | 74 | true |
El byte 72 todavía cuenta. El byte 73 es el primero que ya no. El corte es abrupto, sin mezcla parcial de los bytes sobrantes. Comprobarlo en local contra tu propia biblioteca cuesta un par de minutos.
Los caracteres no son bytes
bcrypt cuenta bytes UTF-8, y tus usuarios escriben caracteres. En ASCII las dos cifras coinciden por casualidad, y justo por eso esto muerde a los equipos en cuanto lanzan fuera de un mercado angloparlante.
| Tipo de carácter | Ejemplo | Bytes por carácter | 72 bytes equivalen a |
|---|---|---|---|
| Letras latinas ASCII | A | 1 | 72 caracteres |
| Caracteres han chinos | 密 | 3 | 24 caracteres |
| Kana japonés | あ | 3 | 24 caracteres |
| Emoji | 🔒 | 4 | 18 caracteres |
| Cirílico | я | 2 | 36 caracteres |
| Diéresis alemanas | ü | 2 | 36 caracteres |
Confirmamos ambos extremos: con una contraseña en chino, las diferencias posteriores al carácter 24 se ignoran (true), y con una contraseña de emoji, las diferencias posteriores al 18 se ignoran (true).
Una frase de contraseña china de 25 caracteres se ve generosa en un campo de contraseña. Ya cruzó la línea. Quien elija 20 emoji lleva dos caracteres por encima del límite y nunca se va a enterar.
Cómo medir la longitud en bytes en tu propio código
Las validaciones de longitud escritas contra el conteo de caracteres van a pasar mientras el valor subyacente ya es demasiado largo. Mide bytes:
# Python
len(pw.encode("utf-8"))
// Node.js
Buffer.byteLength(pw, "utf8")
// Go
len([]byte(pw))
En navegadores sin Buffer, new TextEncoder().encode(pw).length da el mismo número. Pon esta comprobación delante de tu llamada de hashing y devuelve un mensaje de validación de verdad, en vez de dejar que la biblioteca decida por ti a las 3 de la mañana. Y si de paso estás revisando tu política de longitud mínima, cómo se mide realmente la fortaleza de una contraseña explica qué te compra una regla de longitud y qué no.
Por qué también fallan las contraseñas cortas: la sonda de 255 bytes de passlib
Ahora el caso que manda a la mayoría de la gente a los buscadores. Tu contraseña tiene catorce caracteres y la biblioteca insiste en que supera los 72 bytes.
Cómo reproducirlo
Tres líneas, en Python 3.14.5 con bcrypt 5.0.0 y passlib 1.7.4:
from passlib.hash import bcrypt
bcrypt.hash("short-password") # 14 bytes
# ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
Entran catorce bytes, sale una queja sobre 72 bytes. El error de passlib con bcrypt es real, pero el número que aparece en él describe algo totalmente distinto.
La pila de llamadas completa
Esto no es una inferencia: es lo que corre de verdad, rastreado a través de passlib 1.7.4:
- La primera llamada dispara la inicialización del backend:
_calc_checksum→_stub_requires_backend()→set_backend(). _load_backend_mixinleebcrypt.__about__.__version__. El atributo no existe, así que se lanza unAttributeError. passlib se lo traga e imprime(trapped) error reading bcrypt version.- La inicialización continúa hacia
_finalize_backend_mixin(passlib/handlers/bcrypt.py:421), que llama adetect_wrap_bug(IDENT_2A). detect_wrap_bug(mismo archivo,:378) verifica una sonda fija de 255 bytes.- bcrypt 5.0.0 lanza
ValueErrorpara cualquier cosa por encima de 72 bytes, así que la sonda revienta sobre sí misma. - La excepción se propaga hasta el punto donde llamaste. Ves un mensaje sobre 72 bytes que nunca fue sobre tu entrada.
Toda la secuencia ocurre una vez por proceso, en el primer hash o la primera verificación. Por eso el fallo se reproduce siempre y da exactamente igual lo que le pases.
Cómo es la sonda
secret = (b"0123456789" * 26)[:255]
Esa constante viene del bug de envolvimiento del bcrypt de BSD que Openwall divulgó en 2012, donde las claves largas daban la vuelta y colapsaban en hashes más débiles. passlib comprueba al arrancar si el backend que acaba de cargar arrastra ese defecto, y se niega a confiar en un backend que lo tenga.
detect_wrap_bug no es un bug de passlib. Es código defensivo haciendo exactamente aquello para lo que fue escrito, usando un vector de prueba que lleva más de una década siendo válido. Lo que cambió es que bcrypt 5.0.0 ahora trata una entrada de 255 bytes como un error en lugar de como algo que hashear, lo que convierte un autodiagnóstico que pasaba en uno que no hay forma de superar. La discusión de pyca/bcrypt en el issue #1082 cubre el choque entre ambas bibliotecas.
Por qué el parche __about__ no lo arregla
Busca este error y te dirán, una y otra vez, que bcrypt eliminó __about__ y que restaurarlo repara passlib. Ambas mitades son falsas, y esta es la medición que lo demuestra:
| Versión | hasattr(bcrypt, "__about__") | Imprime el aviso trapped | passlib funciona |
|---|---|---|---|
| bcrypt 5.0.0 | False | Sí | No (ValueError) |
| bcrypt 4.3.0 | False | Sí | Sí |
bcrypt 4.3.0 tampoco tiene __about__. Imprime la misma línea (trapped) error reading bcrypt version. Y passlib corre sobre él sin quejarse. Por lo tanto, el atributo ausente no es la línea divisoria entre funcionar y estar roto. Lo es el cambio de comportamiento del ValueError en 5.0.0.
Lo que significa que el parche popular no puede funcionar, y no funciona:
import bcrypt, types
bcrypt.__about__ = types.SimpleNamespace(__version__=bcrypt.__version__) # antes de importar passlib
from passlib.hash import bcrypt as pl
pl.hash("short-password")
# sigue dando ValueError: password cannot be longer than 72 bytes, ...
Lo ejecutamos en un proceso limpio, con el parche aplicado antes de import passlib, precisamente para que nadie pueda atribuir el fallo al orden de importación. Sigue fallando. Lo único que consigue el parche es silenciar un aviso inofensivo. La sonda de 255 bytes del paso 4 es una etapa aparte que nunca consultó __about__ en primer lugar, y detona de todos modos.
Qué cambió realmente bcrypt 5.0
El cambio incompatible de bcrypt 5.0 es una línea de comportamiento con un radio de explosión enorme:
| Entrada | bcrypt 4.3.0 | bcrypt 5.0.0 |
|---|---|---|
| 72 bytes | OK | OK |
| 73 bytes | OK (truncado en silencio) | ValueError |
| 100 bytes | OK (truncado en silencio) | ValueError |
| 255 bytes | OK (truncado en silencio) | ValueError |
El truncamiento de la columna 4.x no es una manera de hablar. Bajo 4.3.0, hash(73 bytes) y hash(100 bytes) construidos a partir del mismo prefijo salen iguales: true.
Así que aquí la razón la tiene bcrypt 5.0. Descartar material de clave en silencio es peor que negarse a continuar, y negarse es lo que debería hacer una biblioteca de hashing cuando no puede honrar la entrada que le dieron. Eso no vuelve indoloro el upgrade. Código que llevaba años perdiendo bytes calladamente ahora lanza excepciones, y si esa ruta de código está detrás de passlib, lanza antes incluso de que tu entrada entre en juego.
De esa tabla se siguen dos cosas, y caen sobre equipos distintos. Si llamas a bcrypt directamente, el upgrade es visible: recibes una excepción en el registro o en el login, en una ubicación de código que es tuya, con una traza que apunta a tu propia llamada de hashing. Agrega una comprobación de longitud en bytes delante y lo resuelves en una tarde.
Si pasas por passlib, el upgrade es invisible hasta que es total. El fallo no es proporcional a cuántos de tus usuarios tienen contraseñas largas, porque no depende para nada de la entrada del usuario. Cada hash y cada verificación del proceso falla, desde la primera llamada en adelante, en una base de código donde nada relativo al manejo de contraseñas cambió. Por eso esto llega como un incidente de despliegue y no como un reporte de bug, mientras el texto del error manda a todo el mundo a buscar en el lugar equivocado.
Cómo arreglarlo
Si puedes cambiar el código
Deja passlib y llama a bcrypt directamente. La última versión publicada de passlib fue la 1.7.4 y el proyecto lleva mucho tiempo en silencio, así que esa capa te aporta muy poco en un proyecto que solo necesita bcrypt:
import bcrypt
password = "correct horse battery staple".encode("utf-8")
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
bcrypt.checkpw(password, hashed) # True
Tanto hashpw como checkpw reciben bytes, así que codifica en la frontera y deja que el resto de tu código siga trabajando con str. No hay detección de backend ni sonda de autodiagnóstico por medio, así que tampoco hay un modo de fallo que te hable de una contraseña que nunca escribiste. Si quieres echarle un ojo al hash resultante, o verificar uno que produjo tu aplicación, el generador de bcrypt corre por completo en tu navegador. Los servidores que usan bcrypt para HTTP Basic Auth tienen la misma restricción estructural en otro formato de archivo, algo que recorre la guía de htpasswd.
Si hoy no puedes cambiar el código
Fija la versión por debajo de 5:
bcrypt<5
Verificamos bcrypt 4.3.0 con passlib 1.7.4 y funciona. Eso sí, ten claro qué acabas de comprar. Esto es un torniquete, no una reparación. Te estás quedando en una versión cuyo comportamiento con una contraseña larga en bcrypt consiste en descartar bytes en silencio, que es exactamente el problema que 5.0 salió a detener. Ponle fecha a esa fijación de versión y planifica la mudanza.
Si tus usuarios realmente escriben frases de contraseña largas
Aplica primero un hash SHA-256 a la contraseña, codifica el digest en base64 y luego entrégaselo a bcrypt:
import base64, hashlib, bcrypt
def prehash(password: str) -> bytes:
return base64.b64encode(hashlib.sha256(password.encode("utf-8")).digest())
hashed = bcrypt.hashpw(prehash(pw), bcrypt.gensalt(rounds=12))
bcrypt.checkpw(prehash(pw), hashed)
La salida siempre es de 44 bytes, cómodamente por debajo de 72, sea cual sea la longitud de la entrada. Y devuelve la propiedad que el truncamiento destruía: las dos contraseñas de 82 bytes de la sección inicial, pasadas por aquí, dan checkpw(prehash(p2), hash(prehash(p1))) = False. La colisión desapareció.
El paso de base64 está haciendo un trabajo real, así que no lo quites. Un digest SHA-256 crudo es binario arbitrario y puede contener bytes NUL, que las implementaciones de bcrypt manejan de forma inconsistente. base64 te da una cadena ASCII de longitud fija y libre de NUL. Aplica la misma función en el registro y en el login, o cada hash existente dejará de verificar.
Qué no hacer
Dos movimientos tentadores, ambos dañinos.
El monkey patch de __about__ no funciona. La sección 4 tiene la medición. Si alguien de tu equipo está a punto de pegarlo, esas cuatro líneas le van a ahorrar una tarde.
Truncar tú mismo con pw[:72] es peor que no hacer nada. Convierte un fallo ruidoso otra vez en uno silencioso, y recrea la colisión de la sección 2 dentro de tu propio código. Estarías reimplantando a mano justo el comportamiento que bcrypt 5.0 salió a eliminar y, a diferencia de la versión de la biblioteca, la tuya no va a avisarle nunca a nadie. Si necesitas que las contraseñas largas funcionen, aplica pre-hash. Si no, valida la longitud en bytes y rechaza con un mensaje claro.
Qué pasa con los hashes que ya están en tu base de datos
Qué filas están afectadas
Solo las cuentas cuyos dueños se registraron con una contraseña de más de 72 bytes. Para la mayoría de los productos de consumo ese conjunto es pequeño, y en cualquier cosa que sea solo ASCII suele reducirse a entusiastas de las frases de contraseña. En productos con usuarios que escriben chino, japonés o emoji aplica la sección 3, y el conjunto afectado puede ser mucho mayor de lo que sugiere una auditoría ciega a los bytes.
No puedes identificar esas filas a partir de los hashes. Un digest de bcrypt tiene ancho fijo y no guarda registro alguno de qué tan larga era su entrada. Si registraste la longitud de la contraseña en el momento del alta, ese log es tu único inventario. La mayoría de los equipos no lo hizo, y reconstruirlo después no es posible, así que planifica sobre la base de no saber en vez de sobre una lista.
No puedes recalcularlos en bloque
No hay texto plano que volver a hashear, que es precisamente el punto de almacenar hashes. Así que la migración tiene que ser perezosa: actualiza cada cuenta la próxima vez que su dueño se autentique correctamente, mientras tienes brevemente el texto plano en memoria.
def login(user, password: str) -> bool:
if not verify_legacy(password, user.password_hash):
return False
if needs_rehash(user.password_hash):
user.password_hash = hash_new_scheme(password)
save(user)
return True
Verifica primero con el esquema viejo y solo después vuelve a hashear. Invertir esos dos pasos reescribe el hash almacenado antes de haber confirmado que la contraseña era correcta. Guarda un identificador de esquema junto a cada hash para que needs_rehash sea una comparación de campo y no una adivinanza, y cuenta con una larga cola de cuentas dormidas que nunca inician sesión. A esas las atiendes en el restablecimiento de contraseña, no por la fuerza.
Cuándo vale la pena una migración completa
Si ya estás escribiendo la ruta de rehash perezoso, ese es el momento más barato que vas a tener nunca para cambiar el algoritmo que hay debajo. El techo de 72 bytes no existe en Argon2id, y la comparación a fondo entre Argon2id y bcrypt cubre cuándo el cambio se paga solo y cuándo quedarse en bcrypt es la decisión correcta. La Password Storage Cheat Sheet de OWASP es la referencia contra la que revisar tus parámetros.
No arranques una migración únicamente por este error. Si tus contraseñas están cómodamente por debajo de 72 bytes, bcrypt sigue siendo una elección sólida y la sección 6 ya resolvió tu problema.
Preguntas frecuentes
¿Por qué bcrypt dice que mi contraseña supera los 72 bytes si es corta?
Porque el mensaje habla de la sonda interna de passlib, no de tu contraseña. En la primera llamada, passlib ejecuta detect_wrap_bug con una cadena de prueba fija de 255 bytes. bcrypt 5.0.0 lanza ValueError para cualquier cosa por encima de 72 bytes, así que la sonda falla y el error aflora en el punto donde llamaste. Una contraseña de 14 bytes lo dispara.
¿bcrypt realmente ignora todo lo que va después de los 72 bytes?
Sí: bcrypt ignora por completo cada byte más allá del 72. Dos contraseñas de 82 bytes que comparten sus primeros 72 bytes producen el hash idéntico $2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S, y cada una verifica contra el hash de la otra. El límite es exacto: una diferencia en el byte 72 cambia el hash, una diferencia en el byte 73 no.
¿El límite de 72 bytes es un problema de seguridad?
El límite de 72 bytes de bcrypt sí lo es, para frases de contraseña largas. Cualquiera que conozca los primeros 72 bytes puede añadir bytes arbitrarios y autenticarse, así que cada byte más allá del límite no aporta nada. Para contraseñas de menos de 72 bytes no cambia absolutamente nada. Aplicar pre-hash con SHA-256 elimina la exposición si las entradas largas deben contar por completo.
¿Cuántos caracteres son 72 bytes?
Cuántos caracteres caben en 72 bytes depende de la codificación. 72 letras ASCII, 36 caracteres cirílicos o con diéresis, 24 caracteres han chinos, 24 kana japoneses o 18 emoji. bcrypt cuenta bytes UTF-8 en lugar de caracteres, así que mide con len(pw.encode("utf-8")) en Python o Buffer.byteLength(pw, "utf8") en Node.
¿Parchear __about__ arregla el error de passlib?
No, el parche __about__ no arregla el error de passlib. Aplicamos el parche antes de import passlib en un proceso limpio y el ValueError saltó igual. bcrypt 4.3.0 tampoco tiene __about__ y funciona sin problemas con passlib, lo que prueba que el atributo ausente no es la causa. El parche solo silencia el aviso (trapped) error reading bcrypt version.
¿Debería bajar bcrypt a una versión anterior a 5.0?
Bajar bcrypt por debajo de 5.0 sirve como medida provisional, sí. bcrypt 4.3.0 con passlib 1.7.4 funciona. Pero 4.x trunca en silencio todo lo que pase de 72 bytes, que es el comportamiento que 5.0 salió a detener, así que trata la fijación de versión como algo temporal y muévete a llamar a bcrypt directamente.
¿Puedo truncar yo mismo la contraseña a 72 bytes?
No trunques tú mismo la contraseña a 72 bytes. pw[:72] recrea dentro de tu propio código la colisión descrita arriba, en silencio, sin ningún aviso de la biblioteca que lo detecte. O aplicas pre-hash con SHA-256 y base64 para que las entradas largas sigan siendo distintas, o validas la longitud en bytes por adelantado y rechazas con un mensaje de error claro.
¿Qué pasa con las contraseñas que ya estaban hasheadas antes de que arreglara esto?
Los hashes bcrypt existentes siguen verificando, porque tu ruta de verificación trunca igual que lo hizo la ruta de hashing. Solo quedan debilitadas las cuentas registradas con contraseñas de más de 72 bytes, y no puedes recalcularlas sin el texto plano. Vuelve a hashear de forma perezosa en el siguiente login exitoso, y atiende las cuentas dormidas en el restablecimiento de contraseña.