Skip to content
Torna al blog
Sicurezza

Errore bcrypt 72 byte: perché falliscono anche le password corte

bcrypt 5.0 solleva "password cannot be longer than 72 bytes" con soli 14 byte: colpa della sonda da 255 byte di passlib. Hash bcrypt online.

12 min di lettura

Errore bcrypt 72 byte: perché falliscono anche le password corte

Due problemi diversi producono lo stesso messaggio, e soltanto uno dei due ha a che fare con la tua password.

Se la tua password supera davvero il limite di 72 byte di bcrypt, bcrypt legge i primi 72 byte e scarta il resto. Abbiamo calcolato l’hash di due password da 82 byte che condividevano i primi 72 byte, con un salt fisso. Entrambe hanno prodotto $2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S, e bcrypt.compareSync(p2, hash(p1)) ha restituito true. La seconda password entra nell’account della prima.

Se invece la tua password è palesemente corta e ottieni comunque questo errore:

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

allora il messaggio sbaglia la causa. Con passlib 1.7.4 e bcrypt 5.0.0, lo solleva anche una password da 14 byte.

Il colpevole, in quel caso, è una sonda di autodiagnosi da 255 byte fissi dentro passlib. Parte una volta sola, quando il backend si inizializza, prima ancora che la tua password arrivi alla chiamata di hashing. bcrypt 5.0.0 rifiuta la sonda, l’eccezione risale, e tu leggi una lamentela su una password che nessuno ha digitato.

E la monkey patch __about__ che domina i risultati di ricerca per questo errore non risolve niente. L’abbiamo rieseguita in un processo pulito, applicando la patch prima di import passlib, e il ValueError si è ripresentato identico.

Triage in 30 secondi: in quale caso sei

La tua passwordQuando scatta l’erroreCausaVai a
Più lunga di 72 byteQuando chiami hashDavvero troppo lunga. bcrypt 5.0 solleva un’eccezione, bcrypt 4.x tronca in silenzioSezioni 2 e 3
Sotto i 72 byte, con passlibAlla prima chiamata nel processoLa sonda da 255 byte di passlib. Non c’entra nulla con la tua passwordSezione 4
Contiene cinese, giapponese o emojiSembra corta, non lo èI caratteri non sono byteSezione 3
Ha iniziato a fallire dopo un aggiornamento di dipendenzeDopo il deployIl breaking change di bcrypt 5.0Sezioni 4 e 5

Se sei nella riga 2, salta avanti. Nulla nelle prossime due sezioni ti aiuterà, e la soluzione è un’altra.

Cosa fa alla tua password il limite di 72 byte di bcrypt

Perché bcrypt si ferma a 72

bcrypt è costruito su Blowfish, e passa la tua password come chiave Blowfish. Blowfish espande la chiave in un array P di 18 sottochiavi, ognuna larga 32 bit. Fanno 18 × 4 = 72 byte di materiale di chiave, e il ciclo di espansione torna all’inizio della chiave una volta riempiti tutti e 18 gli slot.

Il tetto è strutturale, e nessun parametro di configurazione lo sposta. Ogni implementazione conforme di bcrypt ha lo stesso limite su qualsiasi piattaforma, ed è per questo che il numero 72 ricompare identico in Python, Node, Go, Java e PHP.

Due password diverse, un solo hash

Il troncamento delle password in bcrypt rende due password diverse indistinguibili al momento del login.

Con bcryptjs 3.0.3 e il salt fisso $2a$10$abcdefghijklmnopqrstuv, abbiamo calcolato l’hash di due password da 82 byte ciascuna:

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

Entrambe hanno prodotto lo stesso digest:

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

Due password diverse, un solo hash: true. E quindi:

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

Chi conosce i primi 72 byte di una passphrase lunga può accodare qualunque cosa e autenticarsi. Ogni byte oltre il confine contribuisce esattamente zero alla robustezza dell’hash memorizzato, per quanta cura ci abbiano messo i tuoi utenti. Se vuoi confrontare un hash che hai già con una password candidata senza mettere in piedi uno script, puoi generare e verificare hash bcrypt nel browser e vedere lo stesso comportamento di persona.

Dove cade davvero il confine

Abbiamo ristretto il taglio un byte alla volta, tenendo un prefisso condiviso e cambiando esattamente un byte dopo di esso:

Byte di prefisso identiciIl byte N+1 differisce aStesso hash?
7071false
7172false
7273true
7374true

Il byte 72 conta ancora, il byte 73 è il primo che non conta più. Non c’è dissolvenza né mescolamento parziale, e riprodurre la verifica sulla tua libreria costa poco se vuoi confermarla in locale.

I caratteri non sono byte

bcrypt conta byte UTF-8, e i tuoi utenti digitano caratteri. Per l’ASCII i due numeri coincidono, ed è proprio per questo che il problema morde i team appena portano il prodotto fuori da un mercato anglofono.

Tipo di carattereEsempioByte per carattere72 byte corrispondono a
Lettere latine ASCIIA172 caratteri
Caratteri Han cinesi324 caratteri
Kana giapponesi324 caratteri
Emoji🔒418 caratteri
Cirillicoя236 caratteri
Dieresi tedescheü236 caratteri

Abbiamo verificato entrambi gli estremi: con una password cinese, le differenze dopo il 24° carattere vengono ignorate (true), e con una password fatta di emoji vengono ignorate quelle dopo il 18° (true).

Una passphrase cinese di 25 caratteri sembra generosa in un campo password. Ha già superato la linea. Un utente che sceglie 20 emoji è oltre il limite da due caratteri e non lo saprà mai.

Misurare la lunghezza in byte nel tuo codice

I controlli di lunghezza scritti sul conteggio dei caratteri passano mentre il valore sottostante è già troppo lungo. Misura i byte:

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

Nei browser dove Buffer non esiste, new TextEncoder().encode(pw).length dà lo stesso numero. Metti questo controllo prima della chiamata di hashing e restituisci un vero messaggio di validazione, invece di lasciar decidere alla libreria alle 3 di notte. Se stai già rivedendo la tua policy sulla lunghezza minima, come si misura davvero la robustezza di una password spiega cosa ti compra una regola sulla lunghezza e cosa no.

Perché falliscono anche le password corte: la sonda da 255 byte di passlib

La tua password è lunga quattordici caratteri e la libreria insiste che supera i 72 byte. È questo il caso che spinge la maggior parte delle persone sui motori di ricerca.

Come riprodurlo

Tre righe, su Python 3.14.5 con bcrypt 5.0.0 e passlib 1.7.4:

from passlib.hash import bcrypt
bcrypt.hash("short-password")   # 14 byte
# ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])

Quattordici byte in ingresso, una lamentela sui 72 byte in uscita. L’errore di passlib con bcrypt è reale, ma il numero che contiene descrive tutt’altro.

Lo stack di chiamate completo

Ecco la sequenza, tracciata dentro passlib 1.7.4:

  1. La prima chiamata innesca l’inizializzazione del backend: _calc_checksum_stub_requires_backend()set_backend().
  2. _load_backend_mixin legge bcrypt.__about__.__version__. L’attributo non esiste, quindi viene sollevato un AttributeError. passlib lo inghiotte e stampa (trapped) error reading bcrypt version.
  3. L’inizializzazione prosegue in _finalize_backend_mixin (passlib/handlers/bcrypt.py:421), che chiama detect_wrap_bug(IDENT_2A).
  4. detect_wrap_bug (stesso file, :378) verifica una sonda fissa da 255 byte.
  5. bcrypt 5.0.0 solleva ValueError per qualsiasi input oltre i 72 byte, quindi la sonda esplode su sé stessa.
  6. L’eccezione risale fino al punto della tua chiamata. Vedi un messaggio sui 72 byte che non ha mai riguardato il tuo input.

L’intera sequenza avviene una volta per processo, al primo hash o alla prima verifica. Per questo il guasto si riproduce sempre allo stesso modo, qualunque cosa tu passi.

Che aspetto ha la sonda

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

Quella costante viene dal bug di wraparound nel bcrypt di BSD che Openwall ha divulgato nel 2012, dove le chiavi lunghe si riavvolgevano e collassavano in hash più deboli. passlib controlla all’avvio se il backend appena caricato porta quel difetto, e si rifiuta di fidarsi di un backend che ce l’ha.

detect_wrap_bug non è un bug di passlib: è codice difensivo che fa esattamente ciò per cui è stato scritto, con un vettore di test valido da oltre un decennio. A cambiare è stato bcrypt 5.0.0, che ora tratta un input da 255 byte come un errore anziché calcolarne l’hash, e così un autotest che passava diventa un’eccezione che non si può intercettare. La discussione nella issue #1082 di pyca/bcrypt copre la collisione tra le due librerie.

Perché la patch __about__ non risolve

Cerca questo errore e ti verrà ripetuto, all’infinito, che bcrypt ha rimosso __about__ e che ripristinarlo ripara passlib. Entrambe le metà sono sbagliate, ed ecco la misurazione che lo dimostra:

Versionehasattr(bcrypt, "__about__")Stampa l’avviso trappedpasslib funziona
bcrypt 5.0.0FalseNo (ValueError)
bcrypt 4.3.0False

Nemmeno bcrypt 4.3.0 ha __about__. Stampa la stessa riga (trapped) error reading bcrypt version. E passlib ci gira sopra senza protestare. A separare funzionante da rotto è il cambio di comportamento del ValueError in 5.0.0. L’attributo mancante è un depistaggio.

La patch popolare non può funzionare, e infatti non funziona:

import bcrypt, types
bcrypt.__about__ = types.SimpleNamespace(__version__=bcrypt.__version__)  # prima di importare passlib
from passlib.hash import bcrypt as pl
pl.hash("short-password")
# ancora ValueError: password cannot be longer than 72 bytes, ...

L’abbiamo eseguita in un processo pulito, con la patch applicata prima di import passlib, proprio perché nessuno possa attribuire il fallimento all’ordine degli import. Fallisce comunque. L’unica cosa che la patch ottiene è zittire un avviso innocuo. La sonda da 255 byte del passo 4 è una fase separata che non ha mai consultato __about__, e detona in ogni caso.

Cosa ha cambiato davvero bcrypt 5.0

Il breaking change di bcrypt 5.0 è una sola riga di comportamento con un raggio d’azione enorme:

Inputbcrypt 4.3.0bcrypt 5.0.0
72 byteOKOK
73 byteOK (troncato in silenzio)ValueError
100 byteOK (troncato in silenzio)ValueError
255 byteOK (troncato in silenzio)ValueError

Il troncamento nella colonna 4.x l’abbiamo verificato: con 4.3.0, hash(73 byte) e hash(100 byte) costruiti dallo stesso prefisso escono uguali, true.

Quindi bcrypt 5.0 è la più corretta delle due. Scartare in silenzio materiale di chiave è peggio che rifiutarsi di procedere, ed è proprio quello che una libreria di hashing dovrebbe fare quando non può onorare l’input ricevuto. Il che non rende l’aggiornamento indolore. Codice che perdeva byte in silenzio da anni ora solleva eccezioni, e se quel percorso di codice sta dietro passlib, l’eccezione arriva prima ancora che il tuo input entri in gioco.

Le conseguenze ricadono su due tipi di team. Se chiami bcrypt direttamente, l’aggiornamento è visibile: ottieni un’eccezione in registrazione o in login, in un punto del codice che è tuo, con uno stack trace che punta alla tua chiamata di hashing. Aggiungi un controllo sulla lunghezza in byte prima di essa e hai chiuso in un pomeriggio.

Se passi da passlib, l’aggiornamento è invisibile finché non è totale. Il guasto non è proporzionale a quanti dei tuoi utenti hanno password lunghe, perché non dipende affatto dall’input dell’utente. Ogni hash e ogni verifica nel processo falliscono, dalla prima chiamata in poi, su una codebase in cui nulla della gestione password è cambiato. Ecco perché questo si presenta come un incidente di deploy invece che come una segnalazione di bug, e perché il testo dell’errore manda le persone a cercare esattamente nel posto sbagliato.

Come risolverlo

Se puoi modificare il codice

Abbandona passlib e chiama bcrypt direttamente. L’ultima release di passlib è la 1.7.4 e il progetto è fermo da parecchio, quindi quello strato aggiunge molto poco a un progetto che ha bisogno solo di 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 accettano entrambi byte, quindi codifica al confine e lascia che il resto del codice continui a lavorare con str. Chiamato così, bcrypt non rileva backend e non esegue alcuna sonda di autotest, così nessun guasto ti parlerà di una password che non hai fornito. Se vuoi dare un’occhiata all’hash risultante, o verificarne uno prodotto dalla tua applicazione, il generatore bcrypt gira interamente nel tuo browser. I server che usano bcrypt per HTTP Basic Auth hanno lo stesso vincolo strutturale in un formato di file diverso, come illustra la guida a htpasswd.

Se oggi non puoi modificare il codice

Fissa la versione sotto la 5:

bcrypt<5

Abbiamo verificato bcrypt 4.3.0 con passlib 1.7.4 e funziona. Tieni però presente cosa hai comprato: un laccio emostatico, mentre la ferita resta aperta. Resti su una versione il cui comportamento con una password lunga è scartare byte in silenzio, cioè il problema che la 5.0 è stata rilasciata per fermare. Metti una data su quel pin e pianifica il passaggio.

Se i tuoi utenti digitano davvero passphrase lunghe

Applica prima un hash SHA-256 alla password, codifica il digest in base64, poi passa il risultato 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)

L’output è sempre di 44 byte, comodamente sotto i 72, qualunque sia la lunghezza dell’input. E ripristina la proprietà che il troncamento distruggeva: le due password da 82 byte della sezione iniziale, passate di qui, danno checkpw(prehash(p2), hash(prehash(p1))) = False. La collisione è sparita.

Il passaggio base64 fa un lavoro reale, quindi non ometterlo. Un digest SHA-256 grezzo è binario arbitrario e può contenere byte NUL, che le implementazioni di bcrypt gestiscono in modo incoerente. base64 ti dà una stringa ASCII di lunghezza fissa e priva di NUL. Applica la stessa funzione in registrazione e in login, altrimenti ogni hash esistente smette di verificare.

Cosa non fare

Le due mosse che i team provano per prime peggiorano la situazione.

La monkey patch __about__ non funziona. La sezione 4 contiene la misurazione. Se qualcuno nel tuo team sta per incollarla, quelle quattro righe gli risparmieranno un pomeriggio.

Troncare da solo con pw[:72] è peggio che non fare nulla. Riconverte un guasto rumoroso in uno silenzioso, e ricrea nel tuo codice la collisione della sezione 2. Staresti riscrivendo a mano proprio il comportamento che bcrypt 5.0 è stato rilasciato per eliminare e, a differenza della versione della libreria, il tuo non avviserà mai nessuno. Se hai bisogno che le password lunghe funzionino, usa il pre-hash. Se non ne hai bisogno, valida la lunghezza in byte e rifiuta con un messaggio chiaro.

E gli hash già presenti nel database

Quali righe sono interessate

Solo gli account i cui proprietari si sono registrati con una password oltre i 72 byte. Per la maggior parte dei prodotti di consumo è un insieme ristretto, e in un contesto solo ASCII sono di solito appassionati di passphrase. Per prodotti con utenti che digitano cinese, giapponese o emoji vale la sezione 3, e l’insieme colpito può essere molto più ampio di quanto suggerisca un audit cieco ai byte.

Non puoi identificare queste righe partendo dagli hash. Un digest bcrypt ha larghezza fissa e non conserva traccia della lunghezza del suo input. Se hai registrato la lunghezza della password al momento della registrazione, quel log è il tuo unico inventario. La maggior parte dei team non l’ha fatto, e ricostruirlo a posteriori non è possibile: metti in conto di non sapere quali sono.

Non puoi ricalcolare in blocco

Non esiste alcun testo in chiaro da ripassare all’hash, che è poi il senso di memorizzare hash. La migrazione deve essere pigra: aggiorna ogni account la volta successiva in cui il proprietario si autentica con successo, mentre tieni brevemente il testo in chiaro in 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 prima con lo schema vecchio, e solo dopo ricalcola l’hash. Invertire questi due passi riscrive l’hash memorizzato prima di aver confermato che la password era corretta. Salva un identificativo di schema accanto a ogni hash, così needs_rehash diventa un confronto tra campi invece che un’ipotesi, e aspettati una lunga coda di account dormienti che non faranno mai login. Quelli li gestisci al reset della password, senza forzare la migrazione.

Quando conviene una migrazione completa

Se stai già scrivendo il percorso di rehash pigro, quello è il momento più economico che avrai mai per cambiare l’algoritmo sottostante. Il tetto dei 72 byte non esiste in Argon2id, e il confronto approfondito tra Argon2id e bcrypt spiega quando il cambio si ripaga e quando restare su bcrypt è la scelta giusta. L’OWASP Password Storage Cheat Sheet è il riferimento con cui confrontare i tuoi parametri.

Non avviare una migrazione solo per via di questo errore. Se le tue password stanno tranquillamente sotto i 72 byte, bcrypt resta una scelta solida e la sezione 6 ha già risolto il tuo problema.

FAQ

Perché bcrypt dice che la mia password supera i 72 byte quando è corta?

Perché il messaggio riguarda la sonda interna di passlib, non la tua password. Alla prima chiamata, passlib esegue detect_wrap_bug con una stringa di test fissa da 255 byte. bcrypt 5.0.0 solleva ValueError per qualsiasi cosa superi i 72 byte, quindi la sonda fallisce e l’errore emerge nel punto della tua chiamata. Basta una password da 14 byte a innescarlo.

bcrypt ignora davvero tutto ciò che sta dopo i 72 byte?

Sì, completamente. Due password da 82 byte che condividono i primi 72 byte producono l’hash identico $2a$10$abcdefghijklmnopqrstuu.hioaszd4nGKdJlcuRzR1xqPIcN/X.S, e ciascuna verifica contro l’hash dell’altra. Il confine è netto: una differenza al byte 72 cambia l’hash, una differenza al byte 73 no.

Il limite di 72 byte è un problema di sicurezza?

Lo è, per le passphrase lunghe. Chiunque conosca i primi 72 byte può accodare byte arbitrari e autenticarsi, quindi ogni byte oltre il limite non aggiunge nulla. Per password sotto i 72 byte non cambia assolutamente niente. Il pre-hash con SHA-256 elimina l’esposizione se gli input lunghi devono contare per intero.

A quanti caratteri corrispondono 72 byte?

Dipende dalla codifica. 72 lettere ASCII, 36 caratteri cirillici o con dieresi, 24 caratteri Han cinesi, 24 kana giapponesi, oppure 18 emoji. bcrypt conta byte UTF-8 e non caratteri, quindi misura con len(pw.encode("utf-8")) in Python o Buffer.byteLength(pw, "utf8") in Node.

Applicare la patch a __about__ risolve l’errore di passlib?

No. Abbiamo applicato la patch prima di import passlib in un processo pulito e il ValueError è scattato lo stesso. Anche bcrypt 4.3.0 non ha __about__ e funziona bene con passlib, il che dimostra che l’attributo mancante non è la causa. La patch si limita a zittire l’avviso (trapped) error reading bcrypt version.

Devo scendere a una versione di bcrypt sotto la 5.0?

Come tampone, sì. bcrypt 4.3.0 con passlib 1.7.4 funziona. Ma la 4.x tronca in silenzio tutto ciò che supera i 72 byte, cioè il comportamento che la 5.0 è stata rilasciata per fermare: tratta il pin come temporaneo e passa a chiamare bcrypt direttamente.

Posso semplicemente troncare la password a 72 byte da solo?

No. pw[:72] ricrea la collisione descritta sopra dentro il tuo codice, in silenzio, senza alcun avviso della libreria a intercettarla. O applichi il pre-hash con SHA-256 e base64 così gli input lunghi restano distinti, oppure validi la lunghezza in byte a monte e rifiuti con un messaggio d’errore chiaro.

Che succede alle password già sottoposte ad hash prima della correzione?

Continuano a verificare, perché il tuo percorso di verifica tronca esattamente come faceva quello di hashing. Solo gli account registrati con password oltre i 72 byte sono indeboliti, e non puoi ricalcolarli senza il testo in chiaro. Ricalcola l’hash pigramente al prossimo login riuscito, e gestisci gli account dormienti al reset della password.

Tag: bcrypt password-hashing passlib python debugging security

Articoli correlati

Vedi tutti gli articoli