Codifica, hashing e cifratura sono tre cose diverse

In che cosa si distinguono codifica, hashing e cifratura, a che cosa servono davvero Base64, la codifica percentuale e l'escape HTML, e come riconoscere a colpo d'occhio una stringa sconosciuta.

Una stringa che sembra ingarbugliata non è una stringa protetta. Tre operazioni distinte producono un risultato illeggibile, e confonderle è il punto da cui nascono i veri bug di sicurezza.

Reversibile?Serve una chiave?A che cosa serve
CodificaSì, da parte di chiunqueNoFar passare dei byte in un canale che non può trasportarli grezzi
HashingNoNoImpronte, controlli di integrità, chiavi di ricerca
CifraturaSì, con la chiaveSegretezza

La codifica è un cambio di alfabeto. Base64, la codifica percentuale, l'esadecimale e le entità HTML scrivono la stessa informazione in modo diverso, così che un parser più a valle non ci si strozzi. Non c'è di mezzo alcun segreto, quindi non c'è nulla da custodire. Anche lo spostamento di Cesare sta qui: lo strumento Cifrario di Cesare prova per forza bruta tutti e 25 gli spostamenti in una volta sola, il che riassume bene quanto protegga una sostituzione fissa. L'hashing mappa qualsiasi input su un digest di lunghezza fissa e non può essere percorso all'indietro. La cifratura è l'unica delle tre a fornire riservatezza, e comporta sempre una chiave. Se non riesci a indicare la chiave, non si sta cifrando nulla.

Base64 e quanto costa

Base64 divide tre byte (24 bit) in quattro gruppi da sei e scrive ogni gruppo come un carattere preso da un alfabeto di 64 caratteri. Quattro caratteri ogni tre byte significa che il risultato è un terzo più grande. La cosa conta soprattutto quando si incorporano dei file: lo strumento Da file a Base64 produce un data URL, quindi un'immagine da 300 KB finisce nel tuo HTML a circa 400 KB, riscaricata a ogni pagina.

Quando l'input non è un multiplo di tre byte, l'ultimo gruppo viene riempito con =:

"abc"  ->  YWJj      (3 bytes, no padding)
"ab"   ->  YWI=      (2 bytes, one =)
"a"    ->  YQ==      (1 byte, two =)

L'alfabeto standard finisce con + e /, il che è un problema dentro un URL, perché + nei dati di un form si decodifica come uno spazio e / è un separatore di percorso. Esiste quindi un secondo alfabeto: Base64url scambia + con - e / con _, e di solito elimina il riempimento. È il motivo per cui un token copiato da un URL a volte fallisce in un decodificatore impostato sull'alfabeto standard. Il Codificatore Base64 gestisce entrambi, e gestisce correttamente Unicode, cosa che le implementazioni ingenue spesso non fanno: il testo va codificato in UTF-8 prima del passaggio Base64.

Nulla di tutto questo è una misura di sicurezza. Un valore che un prodotto chiama identificatore «cifrato» dentro un URL è molto spesso il Base64 di un semplice numero intero che chiunque può leggere, cambiare e ricodificare in pochi secondi. Lo stesso vale per il payload di un JSON Web Token: il Decodificatore JWT lo legge senza chiave, perché non c'è nulla da sbloccare.

La codifica percentuale, e quale funzione usare

La codifica percentuale sostituisce un byte con % e due cifre esadecimali. Lavora sui byte anziché sui caratteri, quindi il testo non ASCII viene prima codificato in UTF-8: é diventa %C3%A9.

I caratteri non riservati (A-Z, a-z, 0-9, -, ., _, ~) non vanno mai codificati. I caratteri riservati (: / ? # [ ] @ ! $ & ' ( ) * + , ; =) sono i delimitatori che danno struttura a un URL, e se uno di essi vada codificato dipende dal fatto che lo si stia usando come delimitatore o come dato. È questa la differenza fra le due funzioni JavaScript:

encodeURI("https://ex.com/s?q=cats & dogs")
// https://ex.com/s?q=cats%20&%20dogs   the & still splits the query

encodeURIComponent("cats & dogs")
// cats%20%26%20dogs                    safe as a single value

encodeURI serve per un URL intero che vuoi rendere valido, quindi lascia intatti i delimitatori. encodeURIComponent serve per un singolo valore che finisce in un segmento di percorso o in un parametro di query, quindi li codifica. Usa la seconda per qualsiasi cosa tu stia inserendo, e nota che lascia comunque in pace !, ', (, ) e *.

Poi c'è il segno più. I corpi dei form inviati come application/x-www-form-urlencoded codificano uno spazio come +, e la convenzione è tracimata nelle query string, quindi la maggior parte dei server decodifica un + in una query come uno spazio. Un più letterale va inviato come %2B, ed è per questo che alex+billing@example.com arriva così spesso come alex billing@example.com, con l'etichetta distrutta. In un segmento di percorso, al contrario, + è semplicemente un più e uno spazio è %20. Il Codificatore URL rende la cosa visibile quando un valore sopravvive a un giro e si rompe a quello successivo.

L'escape dipende da dove finisce il valore

Non esiste un'unica forma «con escape per il web» di una stringa. Decide il contesto.

ContestoTrattamento corretto
Testo HTMLEntità per &, < e >
Attributo fra virgoletteEntità per & e per la virgoletta che lo racchiude, e metti sempre l'attributo fra virgolette
Dentro <script>Escape delle stringhe JavaScript, non entità
URL in href o srcCodifica il valore in percentuale, applica l'escape da attributo, poi controlla lo schema
Valore CSSEscape CSS, e non costruire mai url() a partire da input dell'utente

Il caso dello script coglie molti alla sprovvista. Il contenuto di uno script non viene sottoposto a decodifica delle entità, quindi un'entità scritta lì dentro resta letterale, e il parser HTML chiude il blocco alla sequenza </script ovunque compaia, dentro una stringa fra virgolette o no:

<script>var s = "</script>";</script>   <!-- block ends early -->
<script>var s = "<\/script>";</script>  <!-- correct -->

Nemmeno per gli URL il solo escape basta. Un valore che inizia con javascript: viene eseguito al clic, per quanto accuratamente sia stato codificato, quindi verifica lo schema rispetto a un elenco di quelli ammessi. Lo strumento Entità HTML copre i casi del testo e degli attributi e riporta indietro le entità decodificandole, cosa che serve di solito quando un'API ha applicato un doppio escape trasformando qualcosa in &amp;amp;.

Hashing: quale algoritmo, e per che cosa

AlgoritmoDigestStato
MD5128 bitCompromesso. Le collisioni sono banali
SHA-1160 bitCompromesso. Le collisioni con prefisso scelto sono praticabili e a buon mercato
SHA-256, SHA-512256, 512 bitVanno bene per integrità e firme
bcrypt, scrypt, Argon2variabileSolo password

MD5 e SHA-1 falliscono sulla resistenza alle collisioni, cioè un attaccante può costruire due input diversi con lo stesso digest. Ovunque un hash faccia le veci di un documento sono inutilizzabili: firme, certificati, indirizzamento per contenuto, deduplicazione di qualsiasi cosa fornita da un attaccante. Nessuno dei due è compromesso sulle preimmagini, quindi un vecchio checksum MD5 individua ancora una corruzione accidentale, ma nessuno dei due ha posto in un lavoro nuovo. Usa SHA-256, che il Generatore di hash calcola sia per i file sia per il testo.

Le password sono un problema diverso, e un hash veloce è lo strumento sbagliato proprio perché è veloce. L'hardware comune esegue miliardi di operazioni SHA-256 al secondo, quindi una tabella trafugata di hash SHA-256 di password è una tabella trafugata di password. bcrypt, scrypt e Argon2id sono deliberatamente lenti e regolabili, con un fattore di lavoro (e per gli ultimi due un costo in memoria) che alzi man mano che l'hardware migliora, e ciascuno memorizza nel proprio risultato un salt casuale univoco.

Un hash non è nemmeno una firma. Per dimostrare che un messaggio proviene da qualcuno che possiede una chiave, usa HMAC-SHA-256 invece di calcolare l'hash del segreto e del messaggio incollati insieme, cosa che è vulnerabile all'estensione di lunghezza.

Che cosa un hash non può fare

Un hash non può essere invertito con un calcolo. Può essere invertito con una ricerca ogni volta che l'insieme degli input possibili è abbastanza piccolo da poter essere enumerato: calcola l'hash di ogni PIN a quattro cifre e hai istantaneamente tutti e diecimila i digest, e lo stesso vale per ogni codice postale, numero di telefono o indirizzo in un elenco trafugato. Calcolare l'hash di un identificatore non lo rende anonimo.

Il salting, cioè un valore casuale univoco conservato insieme a ogni record e mescolato nell'input, non rende più difficile indovinare un singolo bersaglio, ma costringe l'attaccante ad attaccare ogni record separatamente e rende inutili le tabelle precalcolate. Per i valori a bassa entropia, il pepper (una chiave segreta tenuta fuori dal database) è la parte che aiuta davvero.

Riconoscere una stringa sconosciuta

FormaIndizioEsempio
EsadecimaleSolo 0-9a-f, lunghezza pari. 32 caratteri sono MD5, 40 SHA-1, 64 SHA-2565d41402abc4b2a76b9719d911017c592
Base64Maiuscole e minuscole miste con + e /, lunghezza multipla di 4, può finire con = o ==SGVsbG8sIHdvcmxkIQ==
Base64urlLo stesso ma con - e _, di solito senza riempimentoeyJzdWIiOiIxMjM0NSJ9
Con codifica percentuale% seguito da due cifre esadecimalia%20b%26c
JWTTre blocchi Base64url separati da punti, che iniziano con eyJeyJhbGciOi...
bcryptEsattamente 60 caratteri, che iniziano con $2a$, $2b$ o $2y$ seguito da un costo$2b$12$...
Argon2$argon2id$v=19$m=...,t=...,p=...$salt$hash

Vale la pena memorizzare eyJ: è il Base64 di {", quindi qualsiasi blocco che inizia così è un oggetto JSON codificato, che sia un token o no.

Una decodifica riuscita non dimostra che l'ipotesi fosse giusta, perché Base64 trasforma qualsiasi input in byte. Verifica che il risultato sia testo plausibile, oppure che inizi con una firma di file nota: un Base64 che inizia con iVBORw0KGgo è un PNG, /9j/ un JPEG, JVBERi0 un PDF e UEsDB uno zip. Se i byte non assomigliano a nulla, passa il risultato allo strumento Da testo a binario per leggere i codici direttamente, oppure controlla se il valore è stato codificato in percentuale prima di essere codificato in Base64, un doppio incarto piuttosto comune.