Sicurezza essenziale: mai fidarsi di nessuno
15 - Sicurezza essenziale: mai fidarsi di nessuno
Il registro della Guardia funziona, e c'è di che essere fieri. Ora però mettiti per un attimo dall'altra parte: apri il terminale e prova questo.
curl -X DELETE http://localhost:3000/guardiani/1Congratulazioni: hai appena congedato Jon Snow. Senza password, senza permesso, senza che nessuno ti chiedesse niente. E con la CORS aperta a tutti, poteva farlo chiunque su Internet.
Il nostro server è ingenuo. In questo capitolo gli insegniamo la diffidenza, perché la sicurezza non è una feature da aggiungere alla fine: è un'attitudine, e il momento di prenderla è adesso, sul primo server della tua vita.
🛂 La regola d'oro, applicata: validare tutto
"Mai fidarsi del client" te l'ho ripetuto dal capitolo 10, e nel capitolo
scorso l'abbiamo applicata una prima volta: il 400 quando manca il nome.
Ma la validazione seria va oltre il "c'è o non c'è". Rafforziamo il nostro
POST /guardiani:
1app.post('/guardiani', (request, reply) => {
2 const { nome, ruolo } = request.body;
3
4 // esiste, è una stringa, non è vuota, non è chilometrica
5 if (typeof nome !== 'string' || !nome.trim() || nome.length > 100) {
6 return reply.code(400).send({ errore: 'Nome non valido' });
7 }
8
9 const guardiano = {
10 id: prossimoId++,
11 nome: nome.trim(),
12 ruolo: ruolo === 'attendente' ? 'attendente' : 'recluta',
13 };
14 guardiani.push(guardiano);
15 return reply.code(201).send(guardiano);
16});Nota i tre livelli: il tipo (che sia una stringa e non, per dire, un
oggetto malevolo), il contenuto (non vuota, ripulita con trim), i
limiti (100 caratteri bastano a qualunque nome onesto). E il ruolo non si
accetta più a scatola chiusa: o è un valore che conosciamo, o si diventa
recluta. Il client propone, il server dispone, anche sui dettagli.
Sembra pignoleria? Quando nella prossima parte della guida i dati finiranno in un database, questa abitudine diventerà questione di vita o di morte: esiste un'intera categoria di attacchi, la SQL injection, che vive esattamente dei dati non validati. Ne riparleremo, e quel giorno mi ringrazierai.
🔑 Le password: mai, mai, mai in chiaro
Prima o poi il tuo backend gestirà un login, e con lui le password. La regola non ammette eccezioni: una password non si salva mai in chiaro. Se il database venisse rubato (succede anche ai grandi, più spesso di quanto pensi), il ladro avrebbe le password di tutti. E gli utenti, si sa, riusano ovunque.
La soluzione è l'hashing: una trasformazione a senso unico. Dalla password si ottiene l'hash, ma dall'hash non si torna alla password. Al login non confronti le password: confronti gli hash.
1import bcrypt from 'bcrypt';
2
3// alla registrazione: salvi l'hash, butti via la password
4const hash = await bcrypt.hash(password, 10);
5
6// al login: confronti il tentativo con l'hash salvato
7const valido = await bcrypt.compare(tentativo, hash);Due righe con una libreria collaudata come bcrypt. Il che ci porta a una regola sorella: in sicurezza non si inventa: niente algoritmi fatti in casa, niente "tanto chi vuoi che ci attacchi". Si usano gli strumenti standard, quelli che il mondo intero testa da anni.
🚪 Proteggere le rotte: chi va là?
Torniamo al problema dell'apertura: la DELETE libera per tutti. Il server
deve chiedere le credenziali prima delle operazioni pericolose. Nel capitolo
11 hai visto come viaggiano: l'header Authorization. Ecco una sentinella
minima, con un hook di Fastify che ispeziona ogni richiesta in arrivo:
1app.addHook('onRequest', async (request, reply) => {
2 // le letture restano pubbliche; scritture e cancellazioni no
3 if (request.method !== 'GET') {
4 const token = request.headers.authorization;
5 if (token !== 'Bearer parola-dordine') {
6 return reply.code(401).send({ errore: 'Chi va là?' });
7 }
8 }
9});Riprova la DELETE di prima: 401 Unauthorized. Riprovala con l'header
giusto (-H "Authorization: Bearer parola-dordine"): passa. La coppia
401/403 del capitolo 11, ora, lavora per te.
Un token fisso scritto nel codice è didattica, non sicurezza: nel mondo reale i token si generano al login, scadono, e si usano standard come i JWT (JSON Web Token). Ma il flusso (credenziali nell'header, sentinella sulle rotte, 401 per chi non le ha) è esattamente questo.
🔒 HTTPS: la lettera sigillata
C'è un dettaglio che rende tutto il resto inutile, se manca: il token del paragrafo scorso viaggia dentro la richiesta, e una richiesta HTTP è testo in chiaro. Chiunque sulla strada, il Wi-Fi del bar per dire, può leggerla, password e token compresi.
HTTPS è HTTP dentro una busta cifrata: il contenuto lo leggono solo mittente e destinatario. È il lucchetto nella barra del browser, ed è il motivo per cui oggi è lo standard, non un optional. La buona notizia: non dovrai quasi mai occupartene a mano: le piattaforme di hosting moderne e i certificati gratuiti (Let's Encrypt) lo gestiscono per te. Ti basta la regola: in produzione, sempre e solo HTTPS.
🤫 I segreti fuori dal codice
Ultima abitudine, forse la più facile da sbagliare: la "parola d'ordine" del nostro hook è scritta nel codice. E il codice finisce su Git, e magari su GitHub. Le chiavi, le password del database, i token: i segreti non si committano mai.
Vivono invece nelle variabili d'ambiente: valori che il server riceve dall'esterno all'avvio, senza che stiano nel codice sorgente.
const PAROLA_DORDINE = process.env.PAROLA_DORDINE;In locale si mettono in un file .env (che si aggiunge al .gitignore,
sempre); in produzione si configurano sulla piattaforma di hosting. Il codice
li usa, ma non li conosce.
🧾 L'igiene minima, in una lista
- valida tutto quello che arriva dal client: tipo, contenuto, limiti
- password solo hashate, con librerie standard, mai in chiaro, mai algoritmi fatti in casa
- proteggi le rotte che modificano dati: credenziali nell'header,
401per chi non le ha - HTTPS in produzione, sempre
- segreti nelle variabili d'ambiente, mai nel codice, mai su Git
Non è la sicurezza di una banca: è l'igiene di base, quella che separa un esercizio da qualcosa che puoi mostrare al mondo senza arrossire.
✅ Conclusione
Con questo capitolo la parte backend della guida è completa: sai cosa fa un server, parli HTTP fluentemente, riconosci il CRUD ovunque, hai costruito API vere con Node e Fastify e ora sai anche difenderle. Il cuore dell'app batte, ed è protetto.
Resta la promessa fatta a Samwell: un registro che non lo dimentichi a ogni riavvio. Nella prossima parte della guida entriamo nel mondo dei database: dove i dati vivono per sempre, e dove la regola "mai fidarsi del client" tornerà a farci visita molto presto.