Ogni volta che un database di credenziali finisce online, la differenza tra un incidente gestibile e un disastro dipende da una scelta tecnica fatta mesi prima: come sono state archiviate le password. Salvarle in chiaro, o con un semplice MD5, significa regalare gli account agli attaccanti nel giro di minuti. Usare bcrypt con un cost factor adeguato significa costringerli a una battaglia di forza bruta che, nella pratica, non vincono. Questo tutorial mostra passo dopo passo come implementare l’hashing delle password con bcrypt in Node.js, dalla prima riga di codice fino a una API di autenticazione completa e pronta per la produzione.

Seguirai 12 step concreti, con codice testato, esempi di output reali e una sezione di troubleshooting per gli errori più frequenti. Alla fine avrai un progetto Express funzionante che registra utenti, verifica i login in modo sicuro, gestisce il limite dei 72 byte di bcrypt e ricalcola automaticamente gli hash quando l’hardware diventa più potente. Tempo stimato: circa 30 minuti.

Perché serve bcrypt nel 2026 (e perché non basta una hash function)

Una funzione hash crittografica come SHA-256 è progettata per essere veloce. È esattamente la proprietà sbagliata per le password. Una GPU moderna calcola miliardi di hash SHA-256 al secondo, quindi un attaccante che ruba un database di hash SHA-256 può testare interi dizionari e liste di password trapelate in pochi minuti. Le password non vanno trattate come dati da indicizzare, vanno trattate come segreti da rendere costosi da indovinare.

bcrypt risolve il problema con un approccio deliberatamente lento e adattivo. Si basa sul cifrario Blowfish e introduce un parametro di costo (il cost factor) che determina quante iterazioni il calcolo deve eseguire. Il costo cresce in modo esponenziale: ogni incremento di un’unità raddoppia il tempo di calcolo, perché il lavoro è pari a 2 elevato al valore del cost. Questo rende l’hashing abbastanza veloce per un login legittimo (frazioni di secondo) ma proibitivo per chi prova miliardi di combinazioni.

La parola chiave è “adattivo”. bcrypt esiste dal 1999, ma è ancora raccomandato dall’OWASP nel 2026 proprio perché il cost factor si può aumentare nel tempo. Quando, fra cinque anni, le GPU saranno il doppio più veloci, ti basterà alzare il costo di un’unità per riportare la difesa al punto di partenza. Nessun’altra proprietà di sicurezza invecchia così bene.

bcrypt incorpora inoltre un salt univoco e casuale dentro ogni hash. Il salt impedisce gli attacchi con rainbow table (tabelle precalcolate) e garantisce che due utenti con la stessa password ottengano hash completamente diversi. Non devi gestire il salt separatamente: la libreria lo genera, lo usa e lo memorizza dentro la stringa finale. È uno dei motivi per cui bcrypt resta così semplice da usare correttamente.

Prerequisiti e versioni necessarie

Prima di scrivere codice, assicurati di avere l’ambiente corretto. Questo tutorial usa la libreria nativa bcrypt (binding C++ ad alte prestazioni) e, in alternativa, accenna a bcryptjs per gli ambienti dove non puoi compilare moduli nativi. La tabella seguente riassume cosa ti serve.

ComponenteVersione consigliataNote
Node.js20 LTS o 22 LTSNecessario per il binding nativo di bcrypt
npm10 o superioreIncluso con Node.js
bcrypt6.0.0 (ultima versione)Circa 4,8 milioni di download settimanali
bcryptjsultima versionePura JavaScript, zero dipendenze, per serverless
express4.x o 5.xPer la API di esempio degli step 7-9
Build toolsPython 3 + compilatore CSolo per il pacchetto nativo bcrypt

Verifica la tua versione di Node.js prima di iniziare. Un binding nativo come bcrypt richiede una versione supportata, e le release più vecchie (sotto Node 18) non ricevono più aggiornamenti di sicurezza.

node --version
# Output atteso: v20.x.x oppure v22.x.x

npm --version
# Output atteso: 10.x.x

Se non hai una toolchain C disponibile (capita spesso su immagini Docker minimali o su alcune piattaforme serverless), usa bcryptjs al posto di bcrypt. L’API è quasi identica e tutto il codice di questo tutorial funziona cambiando solo la riga di import. Ne parliamo nello step 11.

Anatomia di un hash bcrypt: come leggere la stringa $2b$

Un hash bcrypt è una stringa autocontenuta di 60 caratteri. Tutto ciò che serve per verificare una password (algoritmo, costo, salt e digest) è impacchettato in quella stringa. Capirne la struttura ti aiuta a fare debug e a non commettere errori di archiviazione nel database.

$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
 |  |  |                     |
 |  |  |                     +-- digest (31 caratteri, l'hash vero e proprio)
 |  |  +------------------------ salt (22 caratteri, generato casualmente)
 |  +--------------------------- cost factor (qui 12, cioe 2^12 iterazioni)
 +------------------------------ identificatore di versione ($2b$)

La tabella riassume i quattro segmenti. Quando li conosci, capisci a colpo d’occhio con quale costo è stato generato un hash e se è il momento di ricalcolarlo.

SegmentoEsempioSignificato
Prefisso versione$2b$Variante moderna e sicura dell’algoritmo
Cost factor12Numero di round, lavoro pari a 2^12 = 4096 iterazioni
Salt22 caratteri base64Valore casuale, diverso per ogni hash
Digest31 caratteri base64Risultato del calcolo Blowfish
Lunghezza totale60 caratteriDimensiona così la colonna del database

Errore frequente: dimensionare la colonna del database a 32 o 40 caratteri pensando a un hash “tipico”. Un hash bcrypt occupa sempre 60 caratteri. Usa VARCHAR(60) come minimo, oppure VARCHAR(72) per avere margine in caso di prefissi futuri. Una colonna troppo corta tronca l’hash in silenzio e rende impossibile ogni login successivo.

bcrypt vs Argon2 vs scrypt: quale algoritmo scegliere

bcrypt non è l’unica opzione. Le tre alternative serie nel 2026 sono bcrypt, Argon2 e scrypt. La differenza fondamentale è il tipo di risorsa che rendono costosa: bcrypt è CPU-hard (consuma cicli di processore), mentre Argon2 e scrypt sono memory-hard (consumano grandi quantità di RAM). La durezza in memoria è più efficace contro gli attacchi con hardware specializzato come gli ASIC, perché la memoria veloce è costosa da scalare.

CaratteristicabcryptArgon2idscrypt
Tipo di durezzaCPUMemoria + CPUMemoria
Anno di nascita19992015 (vincitore PHC)2009
Supporto in Node.jsPacchetto bcryptPacchetto argon2Nativo (crypto.scrypt)
Parametri da regolare1 (cost)3 (memoria, iterazioni, parallelismo)3 (N, r, p)
Limite input72 byteNessun limite praticoNessun limite pratico
Raccomandazione OWASPAccettato, cost >= 10Preferito (19 MiB, 2 iter, 1 par)Accettato

La guida OWASP sullo storage delle password preferisce Argon2id, ma bcrypt resta una scelta pienamente accettata e, per molti progetti, la più pragmatica. È maturo, ha un solo parametro da regolare, è disponibile ovunque ed è praticamente impossibile da configurare male. Se stai partendo da zero e vuoi il massimo, valuta Argon2id. Se vuoi qualcosa di robusto, semplice e a prova di errore, bcrypt è la risposta giusta. Per un confronto sui fondamenti, leggi anche la nostra guida sulle funzioni hash crittografiche.

Step 1-3: Creare il progetto e installare bcrypt

Iniziamo dalle fondamenta. Creiamo una cartella di progetto, inizializziamo npm e installiamo le dipendenze. Tutto il tutorial costruisce un’unica applicazione coerente, quindi i comandi che seguono sono il punto di partenza.

Step 1: Inizializzare la cartella di progetto

mkdir bcrypt-auth-demo
cd bcrypt-auth-demo
npm init -y

# Output:
# Wrote to /percorso/bcrypt-auth-demo/package.json

Step 2: Installare bcrypt ed Express

npm install bcrypt express

# Output atteso (le versioni esatte possono variare):
# added 57 packages, and audited 58 packages in 4s
# found 0 vulnerabilities

Se l’installazione di bcrypt fallisce con un errore di compilazione (node-gyp), salta direttamente allo step 11 dove usiamo bcryptjs, che non richiede alcuna compilazione. Per ora, procediamo con il pacchetto nativo.

Step 3: Abilitare i moduli ES e la struttura base

Apri package.json e aggiungi "type": "module" per usare la sintassi import moderna. Poi crea un file hash.js vuoto. La struttura del progetto è minimale e cresce a ogni step.

// package.json (estratto)
{
  "name": "bcrypt-auth-demo",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "bcrypt": "^6.0.0",
    "express": "^4.21.0"
  }
}

Step 4-6: Hashing, verifica e scelta del cost factor

Ora il cuore del tutorial: trasformare una password in un hash bcrypt e verificarla. Sono tre operazioni distinte, ognuna con la sua funzione asincrona dedicata.

Step 4: Generare un hash con bcrypt

Scrivi questo codice in hash.js. La funzione bcrypt.hash prende la password in chiaro e il cost factor, genera automaticamente un salt casuale e restituisce la stringa di 60 caratteri. Usa sempre la versione asincrona basata su promise: la versione sincrona (hashSync) blocca l’event loop e va evitata in un server.

import bcrypt from 'bcrypt';

const COST = 12;

async function creaHash(password) {
  const hash = await bcrypt.hash(password, COST);
  return hash;
}

const hash = await creaHash('passwordSuperSegreta!');
console.log(hash);
// Output: $2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBk...
// La tua stringa sara diversa: il salt e casuale.

Esegui con node hash.js. Nota che eseguendo il file due volte ottieni due hash diversi, anche con la stessa password. È il salt al lavoro: questa è la proprietà che neutralizza le rainbow table.

Step 5: Verificare una password con bcrypt.compare

Per verificare un login non si decifra l’hash (è impossibile, è una funzione a senso unico). Si passa la password fornita e l’hash memorizzato a bcrypt.compare, che estrae salt e costo dall’hash, ricalcola e confronta. La funzione restituisce un booleano.

async function verifica(password, hashMemorizzato) {
  const corretta = await bcrypt.compare(password, hashMemorizzato);
  return corretta;
}

console.log(await verifica('passwordSuperSegreta!', hash));
// Output: true

console.log(await verifica('passwordSbagliata', hash));
// Output: false

Un dettaglio cruciale di sicurezza: bcrypt.compare esegue un confronto a tempo costante. Non interrompe il calcolo al primo byte diverso, quindi non rivela informazioni tramite il tempo di risposta. Non sostituire mai questa funzione con un confronto manuale di stringhe (===): apriresti la porta agli attacchi di timing.

Step 6: Misurare e scegliere il cost factor giusto

Il cost factor è la decisione di sicurezza più importante. Troppo basso, e l’hash è debole. Troppo alto, e ogni login rallenta e il server diventa vulnerabile ad attacchi di tipo denial of service via login ripetuti. L’obiettivo pratico è che un singolo hash richieda tra 250 e 500 millisecondi sull’hardware di produzione. Misura sempre sul tuo server reale.

import bcrypt from 'bcrypt';

for (const cost of [10, 11, 12, 13, 14]) {
  const inizio = process.hrtime.bigint();
  await bcrypt.hash('benchmark', cost);
  const fine = process.hrtime.bigint();
  const ms = Number(fine - inizio) / 1_000_000;
  console.log(`cost ${cost}: ${ms.toFixed(0)} ms`);
}

// Esempio di output su un core a 2 GHz:
// cost 10: 65 ms
// cost 11: 130 ms
// cost 12: 260 ms
// cost 13: 520 ms
// cost 14: 1040 ms

Nota il raddoppio a ogni incremento: è la natura esponenziale di 2 elevato al cost. La guida OWASP fissa il minimo a 10. Nel 2026, per i login interattivi, il valore predefinito ragionevole è 12, da alzare a 13 o 14 su hardware potente. La tabella seguente riassume i compromessi.

Cost factorIterazioni (2^cost)Tempo indicativoUso consigliato
101.024~65 msMinimo OWASP assoluto
112.048~130 msHardware modesto
124.096~260 msDefault consigliato 2026
138.192~520 msServer moderni
1416.384~1040 msMassima sicurezza, dati sensibili

Step 7-9: Costruire l’API di autenticazione con Express

Mettiamo insieme i pezzi in una API reale. Costruiamo due endpoint: registrazione (/register) e login (/login). Per semplicità usiamo un archivio in memoria, ma il flusso è identico con un database vero come PostgreSQL o MongoDB.

Step 7: L’endpoint di registrazione

Crea un file server.js. L’endpoint di registrazione riceve email e password, genera l’hash con bcrypt e lo memorizza. La password in chiaro non tocca mai il database e non viene mai loggata.

import express from 'express';
import bcrypt from 'bcrypt';

const app = express();
app.use(express.json());

const COST = 12;
const utenti = new Map(); // sostituisci con un database reale

app.post('/register', async (req, res) => {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ errore: 'Email e password obbligatorie' });
  }
  if (utenti.has(email)) {
    return res.status(409).json({ errore: 'Utente gia registrato' });
  }

  const hash = await bcrypt.hash(password, COST);
  utenti.set(email, { email, hash });

  res.status(201).json({ messaggio: 'Registrazione completata' });
});

Step 8: L’endpoint di login

Il login recupera l’utente, confronta la password con bcrypt.compare e risponde. Nota un dettaglio di sicurezza importante: il messaggio di errore è identico sia che l’utente non esista, sia che la password sia sbagliata. Rivelare quale dei due è fallito aiuterebbe un attaccante a enumerare gli account validi.

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const utente = utenti.get(email);

  // Messaggio generico: niente enumerazione degli account
  const erroreGenerico = { errore: 'Credenziali non valide' };

  if (!utente) {
    // Esegui comunque un confronto fittizio per non rivelare nulla via timing
    await bcrypt.compare(password, '$2b$12$invalidinvalidinvalidinvalidinvalidinvalidinv');
    return res.status(401).json(erroreGenerico);
  }

  const corretta = await bcrypt.compare(password, utente.hash);
  if (!corretta) {
    return res.status(401).json(erroreGenerico);
  }

  res.json({ messaggio: 'Login riuscito', email });
});

app.listen(3000, () => console.log('Server in ascolto sulla porta 3000'));

Step 9: Testare l’API con curl

Avvia il server con node server.js e prova gli endpoint da un altro terminale. Ecco il flusso completo registrazione e login con l’output atteso.

curl -X POST http://localhost:3000/register \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"Tornado-Viola-92!"}'
# Output: {"messaggio":"Registrazione completata"}

curl -X POST http://localhost:3000/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"Tornado-Viola-92!"}'
# Output: {"messaggio":"Login riuscito","email":"[email protected]"}

curl -X POST http://localhost:3000/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"sbagliata"}'
# Output: {"errore":"Credenziali non valide"}

Hai un’API di autenticazione funzionante. Per completarla con i token di sessione, leggi la nostra guida sull’autenticazione JWT in Node.js, che si combina perfettamente con il flusso di login appena costruito.

Step 10: Ricalcolare gli hash al login (rehash adattivo)

Qui sfruttiamo la vera superpotenza di bcrypt: l’adattività. Quando alzi il cost factor predefinito (per esempio da 12 a 13), gli hash già nel database restano al vecchio costo. Non puoi ricalcolarli senza la password in chiaro, ma c’è un momento perfetto in cui ce l’hai: il login. La strategia è verificare la password, e se l’hash usa un costo inferiore a quello attuale, rigenerarlo in modo trasparente.

import bcrypt from 'bcrypt';

const COST_ATTUALE = 13;

async function loginConRehash(password, utente) {
  const corretta = await bcrypt.compare(password, utente.hash);
  if (!corretta) return false;

  // Estrai il costo dall'hash memorizzato
  const costoHash = bcrypt.getRounds(utente.hash);

  if (costoHash < COST_ATTUALE) {
    // La password e corretta e l'abbiamo in chiaro: aggiorniamo l'hash
    utente.hash = await bcrypt.hash(password, COST_ATTUALE);
    // salva utente.hash nel database
    console.log(`Hash aggiornato da costo ${costoHash} a ${COST_ATTUALE}`);
  }
  return true;
}

La funzione bcrypt.getRounds legge il cost factor direttamente dalla stringa dell’hash. Con questo schema, il tuo intero database migra gradualmente al nuovo costo man mano che gli utenti accedono, senza interruzioni e senza dover mai chiedere a nessuno di reimpostare la password. È il pattern che separa un’implementazione amatoriale da una professionale.

Step 11: Il limite dei 72 byte e il pre-hashing

bcrypt ha un limite poco conosciuto ma critico: ignora tutto ciò che supera i 72 byte di input. Attenzione, sono 72 byte, non 72 caratteri. Un carattere accentato o un’emoji in UTF-8 può occupare fino a 4 byte, quindi una passphrase lunga può raggiungere il limite prima di quanto pensi. Tutto ciò che eccede viene troncato in silenzio, il che significa che due password diverse ma con gli stessi primi 72 byte risultano equivalenti.

La soluzione raccomandata dall’OWASP è il pre-hashing: prima di passare la password a bcrypt, calcola un digest SHA-256 e codificalo in base64. Lo SHA-256 produce sempre 32 byte, e la sua codifica base64 sta comodamente sotto i 72 byte, eliminando il problema del troncamento qualunque sia la lunghezza originale della password.

import bcrypt from 'bcrypt';
import { createHash } from 'node:crypto';

const COST = 12;

function preHash(password) {
  // SHA-256 -> 32 byte, poi base64 -> ~44 caratteri, sempre sotto i 72 byte
  return createHash('sha256').update(password).digest('base64');
}

async function hashSicuro(password) {
  return bcrypt.hash(preHash(password), COST);
}

async function verificaSicura(password, hash) {
  return bcrypt.compare(preHash(password), hash);
}

// Funziona anche con password lunghissime
const lunga = 'a'.repeat(200) + ' caffe ristretto doppio';
const h = await hashSicuro(lunga);
console.log(await verificaSicura(lunga, h)); // true

Una sola avvertenza: se adotti il pre-hashing, devi applicarlo in modo coerente sia in fase di hashing sia in fase di verifica. E se hai già un database di hash creati senza pre-hashing, non puoi cambiare strategia retroattivamente. In quel caso, migra al nuovo schema al login, con lo stesso pattern di rehash dello step 10.

Usare bcryptjs negli ambienti serverless

Se non puoi compilare moduli nativi (AWS Lambda con runtime limitati, edge function, alcuni container), sostituisci bcrypt con bcryptjs, l’implementazione in pura JavaScript con zero dipendenze. È più lenta del binding nativo, ma a livello di codice cambia solo l’import: tutte le funzioni (hash, compare, getRounds) hanno la stessa firma.

npm install bcryptjs

# Nel codice, cambia solo questa riga:
# import bcrypt from 'bcrypt';
import bcrypt from 'bcryptjs';
# Tutto il resto del tutorial funziona identico.

Step 12: Validazione delle password e blocklist (NIST 2026)

Un hashing impeccabile non serve a nulla se l’utente sceglie “password123”. L’ultimo step aggiunge una validazione conforme alle linee guida NIST SP 800-63B (confermate nella revisione 800-63-4 del 2024). Le regole moderne ribaltano i vecchi dogmi.

Il NIST richiede di accettare password lunghe almeno fino a 64 caratteri e di imporre un minimo di almeno 8 caratteri. Soprattutto, il NIST sconsiglia le regole di composizione obbligatorie (la classica imposizione di maiuscole, numeri e simboli) perché spingono gli utenti verso schemi prevedibili. La regola davvero efficace è un’altra: controllare la password contro una blocklist di password compromesse, trapelate in violazioni passate o troppo comuni.

const BLOCKLIST = new Set([
  'password', '123456', '12345678', 'qwerty',
  'password123', 'admin', 'iloveyou', 'letmein'
  // in produzione: carica una lista reale di password trapelate
]);

function validaPassword(password) {
  if (typeof password !== 'string') return 'Password non valida';
  if (password.length < 8) return 'Minimo 8 caratteri';
  if (password.length > 64) return 'Massimo 64 caratteri';
  if (BLOCKLIST.has(password.toLowerCase())) {
    return 'Questa password e troppo comune o compromessa';
  }
  return null; // null significa: password valida
}

const errore = validaPassword('password');
console.log(errore); // "Questa password e troppo comune o compromessa"

In produzione, sostituisci la blocklist di esempio con un controllo serio. Il servizio “Pwned Passwords” di Have I Been Pwned espone una API basata su k-anonymity che permette di verificare se una password è apparsa in una violazione senza mai inviarla per intero. Combinata con bcrypt, questa validazione copre la maggior parte dei rischi reali sugli account.

5 errori comuni da evitare con bcrypt

La maggior parte delle vulnerabilità non nasce da un difetto di bcrypt, ma da un uso sbagliato. Ecco i cinque errori che vediamo più spesso nelle code review.

  • Cost factor troppo basso o lasciato al default di esempio. Un cost di 4 o 6 trovato in un tutorial vecchio rende l’hash quasi inutile. Nel 2026 usa almeno 12 e misura sul tuo hardware.
  • Usare le versioni sincrone in un server. bcrypt.hashSync e compareSync bloccano l’event loop di Node.js. Con un cost di 12, ogni chiamata congela il server per centinaia di millisecondi. Usa sempre le versioni asincrone con await.
  • Confrontare gli hash a mano. Non usare mai password === hash o un confronto di stringhe. Solo bcrypt.compare garantisce il confronto a tempo costante contro gli attacchi di timing.
  • Ignorare il limite dei 72 byte. Senza pre-hashing, le passphrase lunghe vengono troncate in silenzio e perdono entropia. Applica lo SHA-256 più base64 dello step 11.
  • Dimensionare male la colonna del database. Un hash bcrypt è sempre di 60 caratteri. Una colonna VARCHAR(40) tronca l’hash e rompe ogni login. Usa almeno VARCHAR(60).

Risoluzione dei problemi: 8 errori frequenti

Quando qualcosa non funziona, la tabella seguente copre i sintomi più diffusi durante l’integrazione di bcrypt, con la causa e la soluzione concreta.

SintomoCausa probabileSoluzione
node-gyp rebuild failed durante npm installManca la toolchain C/PythonInstalla build-essential e Python 3, oppure passa a bcryptjs
compare restituisce sempre falseHash troncato dal databaseVerifica che la colonna sia almeno VARCHAR(60)
Error: data and hash arguments requiredUno dei due valori è undefinedControlla che req.body sia parsato (express.json)
Login lentissimo, server bloccatoUso di hashSync/compareSyncPassa alle versioni asincrone con await
Password lunghe accettate ma login fallisceTroncamento a 72 byte non gestitoApplica il pre-hashing SHA-256 più base64
Invalid salt version / hash non validoHash corrotto o generato da altra libreriaRigenera l’hash, verifica il prefisso $2b$
Tempi di hashing diversi tra dev e prodHardware diverso, stesso costMisura e ritara il cost in produzione
Module did not self-registerBinary nativo non compatibile con la versione di NodeEsegui npm rebuild bcrypt dopo un upgrade di Node

Tecniche avanzate e best practice per la produzione

Una volta che le basi funzionano, alcune accortezze portano l’implementazione a livello produzione. Sono dettagli che fanno la differenza durante un audit di sicurezza.

  • Configura il cost via variabile d’ambiente. Tieni il cost factor in process.env.BCRYPT_COST così lo aggiorni senza ridistribuire il codice. Questo abilita anche il rehash adattivo dello step 10.
  • Aggiungi un rate limiting sul login. bcrypt protegge gli hash rubati, non gli attacchi online. Limita i tentativi di login per IP e per account (per esempio 5 tentativi al minuto) per fermare il brute force diretto.
  • Considera un “pepper” applicativo. Oltre al salt (memorizzato nell’hash), un pepper è un segreto globale conservato fuori dal database (in un secret manager o HSM). Se il database trapela ma il pepper no, gli hash restano fuori portata. Si applica tipicamente con un HMAC prima di bcrypt.
  • Non loggare mai le password. Sembra ovvio, ma un middleware di logging troppo verboso o un messaggio di errore può finire per registrare il body della richiesta. Filtra esplicitamente i campi sensibili.
  • Forza HTTPS ovunque. bcrypt protegge la password a riposo, ma in transito la difende solo TLS. Approfondisci nella nostra guida su HTTPS e TLS.
  • Pianifica la rotazione del cost. Rivedi il cost factor una volta all’anno. Se l’hardware è migliorato, alza il valore e lascia che il rehash al login faccia il resto.

Per i sistemi che gestiscono dati particolarmente sensibili, valuta di affiancare a bcrypt una strategia di difesa in profondità: autenticazione a più fattori, monitoraggio dei login anomali e cifratura del database a riposo. L’hashing delle password è un pilastro, non l’intero edificio. Per il quadro completo sulla protezione delle credenziali, leggi la nostra guida sulla sicurezza delle password.

Il progetto completo: tutto il codice insieme

Ecco il file server.js finale che unisce hashing sicuro, pre-hashing, validazione NIST, rehash adattivo e i due endpoint. È un punto di partenza pronto da adattare al tuo database.

import express from 'express';
import bcrypt from 'bcrypt';
import { createHash } from 'node:crypto';

const app = express();
app.use(express.json());

const COST = Number(process.env.BCRYPT_COST) || 12;
const utenti = new Map();
const BLOCKLIST = new Set(['password', '123456', 'qwerty', 'admin']);

const preHash = (p) => createHash('sha256').update(p).digest('base64');

function validaPassword(p) {
  if (typeof p !== 'string' || p.length < 8) return 'Minimo 8 caratteri';
  if (p.length > 64) return 'Massimo 64 caratteri';
  if (BLOCKLIST.has(p.toLowerCase())) return 'Password troppo comune';
  return null;
}

app.post('/register', async (req, res) => {
  const { email, password } = req.body;
  const erroreVal = validaPassword(password);
  if (!email || erroreVal) {
    return res.status(400).json({ errore: erroreVal || 'Email obbligatoria' });
  }
  if (utenti.has(email)) {
    return res.status(409).json({ errore: 'Utente gia registrato' });
  }
  const hash = await bcrypt.hash(preHash(password), COST);
  utenti.set(email, { email, hash });
  res.status(201).json({ messaggio: 'Registrazione completata' });
});

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const utente = utenti.get(email);
  const errore = { errore: 'Credenziali non valide' };

  if (!utente) {
    await bcrypt.compare('x', '$2b$12$invalidinvalidinvalidinvalidinvalidinvalidinv');
    return res.status(401).json(errore);
  }
  const corretta = await bcrypt.compare(preHash(password), utente.hash);
  if (!corretta) return res.status(401).json(errore);

  if (bcrypt.getRounds(utente.hash) < COST) {
    utente.hash = await bcrypt.hash(preHash(password), COST);
  }
  res.json({ messaggio: 'Login riuscito', email });
});

app.listen(3000, () => console.log('Server pronto sulla porta 3000'));

Meno di 50 righe per un sistema di autenticazione che rispetta le linee guida OWASP e NIST del 2026. Questa è la forza di bcrypt: la sicurezza corretta è anche la più semplice da scrivere.

Perché l’hashing conta: la lezione delle violazioni

La teoria diventa concreta quando guardi cosa succede dopo una violazione. Quando un database trapela, il destino delle password dipende interamente da come erano archiviate. Con il testo in chiaro, ogni account è compromesso immediatamente. Con MD5 o SHA-1 non salati, i tool di cracking offline svuotano la lista in poche ore. Con bcrypt e un cost adeguato, anche un attaccante con risorse importanti riesce a recuperare solo una piccola frazione delle password più deboli, e nei tempi lunghi.

Questo è il vero valore di bcrypt: trasforma una violazione catastrofica in un incidente contenibile. Dà a te e ai tuoi utenti il tempo di reagire, cambiare le credenziali e limitare i danni. Per capire come avvengono le fughe di dati e come ridurre la superficie d’attacco, leggi il nostro approfondimento sulle violazioni di dati.

Come riassume la guida OWASP sullo storage delle password, l’obiettivo non è rendere l’hashing impossibile da invertire (è già garantito dalla matematica), ma renderlo così costoso da scoraggiare l’attacco su larga scala. Il cost factor di bcrypt è esattamente la leva che regola quel costo, ed è interamente nelle tue mani.

Domande frequenti su bcrypt in Node.js

Qual è il cost factor ideale per bcrypt nel 2026?

Il valore predefinito ragionevole è 12, che corrisponde a circa 250 millisecondi per hash su un core moderno. La guida OWASP fissa il minimo a 10. Su server potenti puoi salire a 13 o 14. La regola pratica: misura sul tuo hardware di produzione e scegli il costo che mantiene il tempo di hashing tra 250 e 500 millisecondi.

bcrypt o Argon2: quale dovrei usare?

L’OWASP preferisce Argon2id perché è memory-hard, quindi più resistente agli attacchi con hardware specializzato. Tuttavia bcrypt resta pienamente accettato ed è più semplice da configurare correttamente, con un solo parametro. Per un nuovo progetto che vuole il massimo, scegli Argon2id. Per robustezza e semplicità a prova di errore, bcrypt è un’ottima scelta.

Devo memorizzare il salt separatamente dall’hash bcrypt?

No. bcrypt incorpora il salt direttamente nella stringa dell’hash di 60 caratteri. Ti basta salvare quell’unica stringa nel database. La funzione compare estrae automaticamente il salt e il cost factor quando verifica una password. Non serve alcuna colonna separata per il salt.

Perché bcrypt tronca le password a 72 byte?

È una conseguenza del design basato su Blowfish, che usa al massimo 72 byte di input per il setup della chiave. Tutto ciò che eccede viene ignorato. La soluzione raccomandata è il pre-hashing con SHA-256 e codifica base64 prima di passare la password a bcrypt, come mostrato nello step 11. Così nessuna parte della password viene mai persa.

bcrypt o bcryptjs: c’è differenza di sicurezza?

Nessuna differenza a livello di sicurezza dell’algoritmo: entrambi implementano lo stesso bcrypt e producono hash compatibili. La differenza è prestazionale. Il pacchetto bcrypt usa un binding C++ nativo ed è più veloce. bcryptjs è pura JavaScript, più lento, ma non richiede compilazione, ideale per ambienti serverless o edge dove non puoi compilare moduli nativi.

Come aggiorno gli hash quando aumento il cost factor?

Non puoi ricalcolare un hash senza la password in chiaro, ma la ottieni al momento del login. Lo schema è: verifica la password, leggi il costo dell’hash memorizzato con bcrypt.getRounds, e se è inferiore al costo attuale rigenera l’hash con il nuovo valore e salvalo. Il database migra gradualmente senza che gli utenti debbano fare nulla.

È sicuro usare bcrypt.hashSync in produzione?

No, evitalo. Le versioni sincrone bloccano l’event loop di Node.js per tutta la durata del calcolo, centinaia di millisecondi con un cost di 12. Sotto carico, il server diventa irraggiungibile. Usa sempre le versioni asincrone basate su promise con await, che non bloccano l’elaborazione delle altre richieste.

bcrypt protegge dagli attacchi di brute force online?

Solo in parte. bcrypt rende costoso il cracking offline degli hash rubati, ma non ferma chi prova migliaia di password contro il tuo endpoint di login in tempo reale. Per quello servono difese aggiuntive: rate limiting per IP e per account, blocco temporaneo dopo tentativi falliti e, idealmente, autenticazione a più fattori.

Risorse e approfondimenti

Documentazione e standard ufficiali usati in questo tutorial: la OWASP Password Storage Cheat Sheet, le linee guida NIST SP 800-63B, il repository ufficiale node.bcrypt.js su GitHub, la voce bcrypt su Wikipedia e la documentazione del modulo crypto di Node.js per l’alternativa scrypt.

Approfondimenti correlati