Skip to content
Voltar ao blog
Segurança

Erro bcrypt de 72 bytes: por que senhas curtas também falham

bcrypt 5.0 lança "password cannot be longer than 72 bytes" com apenas 14 bytes. A causa é a sonda de 255 bytes do passlib. Gere bcrypt grátis online.

12 min de leitura

Erro bcrypt de 72 bytes: por que senhas curtas também falham

Dois problemas diferentes produzem a mesma mensagem, e só um deles tem alguma relação com a sua senha.

Se a sua senha realmente ultrapassa o limite de 72 bytes do bcrypt, o bcrypt lê os primeiros 72 bytes e descarta o resto. Nós geramos o hash de duas senhas de 82 bytes que compartilhavam os mesmos primeiros 72 bytes, sob um salt fixo. As duas produziram $2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S, e bcrypt.compareSync(p2, hash(p1)) retornou true. A segunda senha entra na conta da primeira.

Se a sua senha é visivelmente curta e mesmo assim você vê isto:

password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])

então a mensagem está errada sobre a causa. Com passlib 1.7.4 e bcrypt 5.0.0, uma senha de 14 bytes provoca esse erro.

O culpado aqui é uma sonda de autoteste fixa de 255 bytes dentro do passlib. Ela roda uma vez, quando o backend é inicializado, antes de a sua senha sequer chegar à chamada de hash. O bcrypt 5.0.0 rejeita a sonda, a exceção escapa, e você lê uma reclamação sobre uma senha que ninguém digitou.

O monkey patch de __about__ que domina os resultados de busca para esse erro também não resolve o problema. Reexecutamos o patch em um processo limpo, aplicado antes de import passlib, e o ValueError voltou exatamente igual.

Triagem em 30 segundos: qual é o seu caso

Sua senhaQuando o erro disparaCausa raizVá para
Maior que 72 bytesAo chamar o hashLonga demais de verdade. bcrypt 5.0 lança erro, bcrypt 4.x trunca em silêncioSeções 2 e 3
Menor que 72 bytes, usando passlibNa primeira chamada do processoA sonda de 255 bytes do passlib. Nada a ver com a sua senhaSeção 4
Contém chinês, japonês ou emojiParece curta, mas não éCaracteres não são bytesSeção 3
Começou a falhar depois de atualizar dependênciasDepois do deployA mudança incompatível do bcrypt 5.0Seções 4 e 5

Se você está na linha 2, pule adiante. Nada nas duas próximas seções vai ajudar, e a correção é outra.

O que o limite de 72 bytes do bcrypt faz com a sua senha

Por que o bcrypt para em 72

O bcrypt é construído sobre o Blowfish, e alimenta a sua senha como chave do Blowfish. O Blowfish expande a chave em um P-array de 18 subchaves, cada uma com 32 bits. Isso dá 18 × 4 = 72 bytes de material de chave, e o laço de expansão volta ao início da chave assim que preenche todos os 18 espaços.

Ou seja, o teto é estrutural. Não é implementação preguiçosa nem um buffer configurável que alguém esqueceu de aumentar. Toda implementação de bcrypt em conformidade, em qualquer plataforma, tem o mesmo limite, e é por isso que o número 72 aparece igual em Python, Node, Go, Java e PHP.

Duas senhas diferentes, um hash

O truncamento de senha do bcrypt tem consequência direta de segurança.

Usando bcryptjs 3.0.3 com o salt fixo $2a$10$abcdefghijklmnopqrstuv, geramos o hash de duas senhas de 82 bytes cada:

SenhaValorBytes
p1"A"×72 + "XXXXXXXXXX"82
p2"A"×72 + "ZZZZZZZZZZ"82

As duas produziram o mesmo digest:

$2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S

Duas senhas diferentes, um hash: true. E a consequência que decorre disso:

bcrypt.compareSync(p2, hash(p1))  // true

Um atacante que conheça os primeiros 72 bytes de uma frase-senha longa pode acrescentar qualquer coisa e se autenticar. Cada byte além da fronteira contribui exatamente zero para a força do hash armazenado, por mais cuidado que os seus usuários tenham tido ao escolhê-los. Se você quiser conferir um hash que já tem contra uma senha candidata sem montar um script, dá para gerar e verificar hashes bcrypt no navegador e observar o mesmo comportamento por conta própria.

Onde a fronteira realmente cai

Estreitamos o corte byte a byte, mantendo um prefixo em comum e alterando exatamente um byte depois dele:

Bytes idênticos no prefixoByte N+1 difere emMesmo hash?
7071false
7172false
7273true
7374true

O byte 72 ainda conta. O byte 73 é o primeiro que não conta. A virada é abrupta, sem mistura parcial no meio do caminho, e repetir esse teste na sua própria biblioteca leva poucos minutos.

Caracteres não são bytes

O bcrypt conta bytes UTF-8, e os seus usuários digitam caracteres. Para ASCII os dois números coincidem, que é justamente por que isso morde as equipes no instante em que elas lançam fora de um mercado de língua inglesa.

Tipo de caractereExemploBytes por caractere72 bytes equivalem a
Letras latinas ASCIIA172 caracteres
Caracteres han chineses324 caracteres
Kana japonês324 caracteres
Emoji🔒418 caracteres
Cirílicoя236 caracteres
Tremas alemãesü236 caracteres

Confirmamos os dois extremos: com uma senha em chinês, diferenças depois do 24º caractere são ignoradas (true); com uma senha em emoji, diferenças depois do 18º são ignoradas (true).

Uma frase-senha em chinês com 25 caracteres parece generosa em um campo de senha. Ela já cruzou a linha. Um usuário que escolhe 20 emoji está dois caracteres acima do limite e nunca será avisado.

Medindo o tamanho em bytes no seu próprio código

Verificações escritas contra contagem de caracteres vão passar mesmo quando o valor por baixo já está longo demais. Meça bytes:

# Python
len(pw.encode("utf-8"))
// Node.js
Buffer.byteLength(pw, "utf8")
// Go
len([]byte(pw))

Em navegadores sem Buffer, new TextEncoder().encode(pw).length dá o mesmo número. Coloque essa verificação antes da sua chamada de hash e devolva uma mensagem de validação de verdade, em vez de deixar a biblioteca decidir por você às 3 da manhã. Se você também estiver revendo a sua política de tamanho mínimo já que está por ali, como a força de uma senha é realmente medida mostra o que uma regra de comprimento compra e o que ela não compra.

Por que senhas curtas também falham: a sonda de 255 bytes do passlib

Agora o caso que manda a maioria das pessoas para os buscadores. Sua senha tem quatorze caracteres e a biblioteca insiste que ela passa de 72 bytes.

Reproduzindo

Três linhas, em Python 3.14.5 com bcrypt 5.0.0 e 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])

Quatorze bytes na entrada, uma reclamação sobre 72 bytes na saída. O erro do bcrypt no passlib é real, mas o número dentro dele descreve outra coisa.

A pilha de chamadas completa

Isto não é dedução. O que roda de fato, rastreado no passlib 1.7.4:

  1. A primeira chamada dispara a inicialização do backend: _calc_checksum_stub_requires_backend()set_backend().
  2. _load_backend_mixinbcrypt.__about__.__version__. O atributo não existe, então um AttributeError é levantado. O passlib engole a exceção e imprime (trapped) error reading bcrypt version.
  3. A inicialização segue para _finalize_backend_mixin (passlib/handlers/bcrypt.py:421), que chama detect_wrap_bug(IDENT_2A).
  4. detect_wrap_bug (mesmo arquivo, :378) verifica uma sonda fixa de 255 bytes.
  5. O bcrypt 5.0.0 levanta ValueError para qualquer coisa acima de 72 bytes, então a sonda explode em cima de si mesma.
  6. A exceção sobe até o seu ponto de chamada. Você vê uma mensagem sobre 72 bytes que nunca teve a ver com a sua entrada.

A sequência inteira acontece uma vez por processo, no primeiro hash ou na primeira verificação. É por isso que a falha é tão reprodutível e tão insensível ao que você passa como argumento.

Como a sonda é por dentro

secret = (b"0123456789" * 26)[:255]

Essa constante vem do bug de wraparound no bcrypt do BSD que a Openwall divulgou em 2012, no qual chaves longas davam a volta e colapsavam em hashes mais fracos. O passlib verifica na inicialização se o backend recém-carregado carrega essa falha, e se recusa a confiar em um backend que a tenha.

detect_wrap_bug não é um bug do passlib. É código defensivo fazendo exatamente aquilo para que foi escrito, usando um vetor de teste válido há mais de uma década. O que mudou foi que o bcrypt 5.0.0 agora trata uma entrada de 255 bytes como erro em vez de algo para hashear, o que transforma um autoteste que passava em um autoteste impossível de capturar. A discussão no issue #1082 do pyca/bcrypt cobre esse choque entre as duas bibliotecas.

Por que o patch de __about__ não resolve

Pesquise esse erro e vão te dizer, repetidamente, que o bcrypt removeu __about__ e que restaurá-lo conserta o passlib. As duas metades disso estão erradas. A medição:

Versãohasattr(bcrypt, "__about__")Imprime o aviso trappedpasslib funciona
bcrypt 5.0.0FalseSimNão (ValueError)
bcrypt 4.3.0FalseSimSim

O bcrypt 4.3.0 também não tem __about__. Ele imprime a mesma linha (trapped) error reading bcrypt version. E o passlib roda em cima dele sem reclamar. Portanto o atributo ausente não é a linha divisória entre funcionar e quebrar. A mudança de comportamento do ValueError no 5.0.0 é.

O que significa que o patch popular não tem como funcionar, e de fato não 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")
# ainda ValueError: password cannot be longer than 72 bytes, ...

Rodamos isso em um processo limpo, com o patch aplicado antes de import passlib, justamente para que ninguém pudesse atribuir a falha à ordem de importação. Continua falhando. Tudo o que o patch consegue é silenciar um aviso inofensivo. A sonda de 255 bytes do passo 4 é uma etapa separada, que nunca consultou __about__ para começo de conversa, e ela detona de um jeito ou de outro.

O que o bcrypt 5.0 mudou de fato

A mudança incompatível do bcrypt 5.0 cabe em uma linha de comportamento e atinge muito código:

Entradabcrypt 4.3.0bcrypt 5.0.0
72 bytesOKOK
73 bytesOK (truncado em silêncio)ValueError
100 bytesOK (truncado em silêncio)ValueError
255 bytesOK (truncado em silêncio)ValueError

O truncamento na coluna do 4.x não é força de expressão. No 4.3.0, hash(73 bytes) e hash(100 bytes) construídos a partir do mesmo prefixo saem iguais: true.

Então o bcrypt 5.0 é a biblioteca mais correta aqui. Descartar material de chave em silêncio é pior do que recusar a entrada, e recusar é o que uma biblioteca de hash deveria fazer quando não consegue honrar o que recebeu. Isso não torna a atualização indolor. Código que vinha perdendo bytes silenciosamente por anos agora lança exceção, e se esse caminho de código está atrás do passlib, ele lança antes mesmo de a sua entrada entrar na história.

Duas coisas decorrem daquela tabela, e elas caem sobre times diferentes. Se você chama o bcrypt diretamente, a atualização é visível: você recebe uma exceção no cadastro ou no login, em um ponto do código que é seu, com um stack trace apontando para a sua própria chamada de hash. Coloque uma verificação de tamanho em bytes antes dela e você resolve em uma tarde.

Se você passa pelo passlib, a atualização é invisível até ser total. A falha não é proporcional a quantos usuários seus têm senhas longas, porque ela não depende da entrada do usuário de jeito nenhum. Todo hash e toda verificação no processo falham, a partir da primeira chamada, em uma base de código onde nada relacionado a senhas mudou. É por isso que isso aparece como incidente de deploy e não como relatório de bug, e por isso que o texto do erro manda as pessoas procurarem exatamente no lugar errado.

Corrigindo

Se você pode mexer no código

Abandone o passlib e chame o bcrypt diretamente. O último lançamento do passlib foi o 1.7.4 e o projeto está parado há muito tempo, então essa camada agrega muito pouco em um projeto que só precisa de bcrypt:

import bcrypt

password = "correct horse battery staple".encode("utf-8")
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))

bcrypt.checkpw(password, hashed)  # True

hashpw e checkpw recebem bytes, então codifique na fronteira e mantenha o resto do seu código trabalhando com str. Não há detecção de backend nem sonda de autoteste, e portanto nenhum erro sobre uma senha que você não forneceu. Se quiser dar uma olhada no hash resultante, ou verificar um que a sua aplicação produziu, o gerador bcrypt roda inteiramente no seu navegador. Servidores que usam bcrypt para HTTP Basic Auth têm a mesma restrição estrutural em outro formato de arquivo, que o guia do htpasswd percorre em detalhe.

Se você não pode mexer no código hoje

Fixe abaixo da 5:

bcrypt<5

Verificamos o bcrypt 4.3.0 com o passlib 1.7.4 e funciona. Mas tenha clareza sobre o que você acabou de comprar. Isso é um torniquete, não um reparo. Você está permanecendo em uma versão cujo comportamento com uma senha longa no bcrypt é descartar bytes em silêncio, que é exatamente o problema que o 5.0 foi lançado para acabar. Ponha uma data nesse pin e planeje a saída.

Se os seus usuários realmente digitam frases-senha longas

Passe a senha primeiro por um SHA-256, codifique o digest em base64 e só então entregue isso ao 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)

A saída tem sempre 44 bytes, confortavelmente abaixo de 72, seja qual for o tamanho da entrada. E ela restaura a propriedade que o truncamento destruía: as duas senhas de 82 bytes da abertura deste texto, passadas por aqui, dão checkpw(prehash(p2), hash(prehash(p1))) = False. A colisão sumiu.

O passo de base64 está fazendo trabalho de verdade, então não o remova. Um digest SHA-256 cru é binário arbitrário e pode conter bytes NUL, que as implementações de bcrypt tratam de forma inconsistente. O base64 te dá uma string ASCII de tamanho fixo e livre de NUL. Aplique a mesma função no cadastro e no login, ou todo hash existente para de verificar.

O que não fazer

Dois movimentos tentadores, ambos nocivos.

O monkey patch de __about__ não funciona. A seção 4 traz a medição. Se alguém no seu time está prestes a colar aquilo, as quatro linhas acima economizam uma tarde dessa pessoa.

Truncar você mesmo com pw[:72] é pior do que não fazer nada. Converte uma falha barulhenta de volta em uma falha silenciosa, e recria a colisão da seção 2 dentro do seu próprio código. Você estaria reimplementando à mão exatamente o comportamento que o bcrypt 5.0 foi lançado para eliminar e, ao contrário da versão da biblioteca, a sua nunca vai avisar ninguém. Se você precisa que senhas longas funcionem, faça pré-hash. Se não precisa, valide o tamanho em bytes e rejeite com uma mensagem clara.

E os hashes que já estão no seu banco

Quais linhas são afetadas

Só as contas cujos donos se cadastraram com uma senha acima de 72 bytes. Para a maioria dos produtos de consumo isso é um conjunto pequeno, e para qualquer coisa somente-ASCII costuma significar entusiastas de frases-senha. Para produtos com usuários digitando chinês, japonês ou emoji, a seção 3 se aplica e o conjunto afetado pode ser bem maior do que uma auditoria cega a bytes sugere.

Você não consegue identificar essas linhas a partir dos hashes. Um digest bcrypt tem largura fixa e não guarda registro algum de quão longa foi a sua entrada. Se você registrou o tamanho da senha no cadastro, esse log é o seu único inventário. A maioria dos times não registrou, e reconstruir isso depois do fato não é possível, então planeje contando com a ignorância, não com uma lista.

Você não pode recalcular em massa

Não existe texto claro para re-hashear, que é justamente o motivo de se armazenar hashes. Então a migração tem que ser preguiçosa: atualize cada conta na próxima vez que o dono se autenticar com sucesso, enquanto você segura brevemente o texto claro em memória.

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

Verifique com o esquema antigo primeiro, e só então re-hasheie. Inverter esses dois passos reescreve o hash armazenado antes de você ter confirmado que a senha estava correta. Guarde um identificador de esquema junto de cada hash para que needs_rehash seja uma comparação de campo e não um palpite, e conte com uma cauda longa de contas dormentes que nunca fazem login. Essas você trata na redefinição de senha, não à força.

Quando vale a pena migrar de vez

Se você já está escrevendo o caminho de re-hash preguiçoso, esse é o momento mais barato que você jamais terá para trocar o algoritmo por baixo dele. O teto de 72 bytes não existe no Argon2id, e a comparação aprofundada entre Argon2id e bcrypt cobre quando a troca se paga e quando ficar no bcrypt é a decisão certa. O OWASP Password Storage Cheat Sheet é a referência contra a qual conferir os seus parâmetros.

Não comece uma migração só por causa desse erro. Se as suas senhas estão confortavelmente abaixo de 72 bytes, o bcrypt continua sendo uma escolha sólida e a seção 6 já resolveu o seu problema.

FAQ

Por que o bcrypt diz que minha senha passa de 72 bytes se ela é curta?

Porque a mensagem é sobre a sonda interna do passlib, não sobre a sua senha. Na primeira chamada, o passlib roda detect_wrap_bug com uma string de teste fixa de 255 bytes. O bcrypt 5.0.0 levanta ValueError para qualquer coisa acima de 72 bytes, então a sonda falha e o erro aflora no seu ponto de chamada. Uma senha de 14 bytes já dispara isso.

O bcrypt realmente ignora tudo depois de 72 bytes?

Sim, o bcrypt ignora completamente todo byte depois do 72. Duas senhas de 82 bytes que compartilham os primeiros 72 bytes produzem o hash idêntico $2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S, e cada uma verifica contra o hash da outra. A fronteira é exata: uma diferença no byte 72 muda o hash, uma diferença no byte 73 não muda.

O limite de 72 bytes é um problema de segurança?

O limite de 72 bytes do bcrypt é um problema para frases-senha longas. Qualquer um que conheça os primeiros 72 bytes pode acrescentar bytes arbitrários e se autenticar, então cada byte além do limite não soma nada. Para senhas abaixo de 72 bytes não muda nada. Fazer pré-hash com SHA-256 elimina a exposição quando entradas longas precisam contar por inteiro.

Quantos caracteres são 72 bytes?

Quantos caracteres cabem em 72 bytes depende da codificação. 72 letras ASCII, 36 caracteres cirílicos ou com trema, 24 caracteres han chineses, 24 kana japoneses, ou 18 emoji. O bcrypt conta bytes UTF-8 e não caracteres, então meça com len(pw.encode("utf-8")) em Python ou Buffer.byteLength(pw, "utf8") em Node.

Aplicar o patch em __about__ corrige o erro do passlib?

Não, o patch em __about__ não corrige o erro do passlib. Aplicamos o patch antes de import passlib em um processo limpo e o ValueError disparou mesmo assim. O bcrypt 4.3.0 também não tem __about__ e funciona bem com o passlib, o que prova que o atributo ausente não é a causa. O patch apenas silencia o aviso (trapped) error reading bcrypt version.

Devo fazer downgrade do bcrypt para abaixo da 5.0?

O downgrade do bcrypt para abaixo da 5.0 serve como paliativo, sim. bcrypt 4.3.0 com passlib 1.7.4 funciona. Mas a 4.x trunca em silêncio tudo o que passa de 72 bytes, que é justamente o comportamento que a 5.0 veio encerrar, então trate o pin como temporário e migre para chamar o bcrypt diretamente.

Posso simplesmente truncar a senha em 72 bytes eu mesmo?

Não trunque a senha em 72 bytes por conta própria. pw[:72] recria a colisão descrita acima dentro do seu próprio código, em silêncio, sem nenhum aviso de biblioteca para capturar. Ou faça pré-hash com SHA-256 e base64 para que entradas longas continuem distintas, ou valide o tamanho em bytes logo na entrada e rejeite com uma mensagem de erro clara.

O que acontece com as senhas que já foram hasheadas antes de eu corrigir isso?

Os hashes bcrypt existentes continuam verificando, porque o seu caminho de verificação trunca do mesmo jeito que o caminho de hash truncava. Só as contas cadastradas com senhas acima de 72 bytes ficam enfraquecidas, e você não consegue recalculá-las sem o texto claro. Re-hasheie de forma preguiçosa no próximo login bem-sucedido, e trate as contas dormentes na redefinição de senha.

Tags: bcrypt password-hashing passlib python debugging security

Artigos relacionados

Ver todos os artigos