"Ma tanto lo passiamo in Base64 prima di salvarlo." Se ti capita di rivedere abbastanza file di configurazione, dashboard interne, bundle del browser, handler di webhook o codice di autenticazione, prima o poi vedi quella frase. Suona plausibile perché l'output sembra rimescolato:
c2VjcmV0IHBhc3N3b3Jk
Ma Base64 non è crittografia. Non è protezione con password. Non è un modo sicuro per nascondere chiavi API, claim di JWT, ID utente, credenziali Basic Auth o dati privati in un URL. Lo stesso valore si decodifica in una sola chiamata:
atob("c2VjcmV0IHBhc3N3b3Jk");
// "secret password"
La regola pratica è netta: se il valore originale torna senza chiave segreta, Base64 non l'ha protetto. Ha solo cambiato la rappresentazione.
Il test di sicurezza veloce
Quando vedi Base64 nel codice, chiediti che lavoro sta facendo:
| Base64 viene usato per... | Buon uso? | Cosa controllare |
|---|---|---|
| Infilare byte in JSON | Sì | Tenere limiti di dimensione ragionevoli |
Incorporare una piccola data: URI |
Sì | Non inlinare file grandi alla cieca |
| Salvare come testo ciphertext, nonce, salt o byte di firma | Sì | Il passo crittografico deve stare prima di Base64 |
| Portare un cursore di paginazione opaco | Di solito | Trattalo come un dettaglio di interfaccia, non come controllo di accesso |
| Costruire un header Basic Auth | Solo con HTTPS | TLS protegge la richiesta, Base64 no |
| Nascondere una chiave API nel frontend | No | Ogni browser riceve valore e decoder |
| Salvare una password | No | Usa hashing lento delle password |
| Nascondere claim di JWT | No | I payload di JWT firmati sono leggibili se non cifrati come JWE |
| Nascondere ID utente o ruoli negli URL | No | L'autorizzazione lato server deve comunque girare |
Se la frase che ti passa per la testa è "così nessuno lo legge", non stai descrivendo Base64. Stai descrivendo crittografia, hashing, firma, controllo di accesso o gestione dei segreti.
Cosa fa davvero Base64
Base64 è uno schema di codifica da binario a testo. RFC 4648 definisce l'alfabeto Base64 tipico come:
A-Z a-z 0-9 + / =
Il carattere = è padding. Base64 raggruppa i byte di input in blocchi da 24 bit: 3 byte in ingresso diventano 4 caratteri stampabili. Ecco perché l'output di Base64 è circa il 33 % più grande dei byte grezzi prima di qualsiasi compressione.
btoa("hello");
// "aGVsbG8="
atob("aGVsbG8=");
// "hello"
Lo stesso round trip funziona da terminale:
printf '%s' 'secret password' | base64
# c2VjcmV0IHBhc3N3b3Jk
printf '%s' 'c2VjcmV0IHBhc3N3b3Jk' | base64 -d
# secret password
Non c'è prompt di password, chiave privata, passo di decifratura, salt, nonce, IV, tag né work factor. Chi ha la stringa codificata può invertirla.
Base64 vs Base64url
Il Base64 standard non è sempre URL-safe perché +, / e = sono scomodi in URL, nomi di file, cookie e segmenti di token. Base64url cambia l'alfabeto:
| Caratteristica | Base64 standard | Base64url |
|---|---|---|
| Caratteri 62 e 63 | + e / |
- e _ |
| Padding | Di solito = |
Spesso omesso |
| Posti tipici | MIME, PEM, Basic Auth, campi binari | JWT, token in URL, nomi di file |
| Differenza di sicurezza | Nessuna | Nessuna |
Questo conta quando si debuggano i JWT. Un JWT usa Base64url, non Base64 standard con padding:
header.payload.signature
Se incolli un segmento di JWT in un decoder Base64 standard e fallisce, la stringa non è per forza segreta o cifrata. Può servire solo un trattamento Base64url:
function base64urlToBase64(part) {
let out = part.replace(/-/g, "+").replace(/_/g, "/");
while (out.length % 4 !== 0) out += "=";
return out;
}
Quella conversione cambia l'alfabeto. Non aggiunge riservatezza.
UTF-8: la trappola del browser
btoa() e atob() del browser lavorano su stringhe binarie. Gli esempi in ASCII puro vanno bene:
btoa("hello");
// "aGVsbG8="
Ma il testo Unicode richiede prima un passo in byte. Questo sorprende i team che testano con hello e poi codificano nomi di cliente, testo in cinese, emoji o arabo:
btoa("你好");
// InvalidCharacterError in browsers
Usa byte UTF-8:
function utf8ToBase64(text) {
const bytes = new TextEncoder().encode(text);
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
return btoa(binary);
}
function base64ToUtf8(base64) {
const binary = atob(base64);
const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
const encoded = utf8ToBase64("你好");
base64ToUtf8(encoded);
// "你好"
Lo strumento Base64 Encode & Decode su questo sito fa il round trip UTF-8 nel browser, così il testo non ASCII torna come testo e non come mojibake.
Perché gli sviluppatori confondono Base64 con la crittografia
Base64 compare spesso vicino a sicurezza vera:
- I certificati TLS sono salvati come testo PEM.
- I JWT usano sezioni Base64url.
- HTTP Basic Auth usa Base64 per
username:password. - Le chiavi pubbliche SSH contengono materiale di chiave codificato in Base64.
- Il ciphertext cifrato viene spesso codificato in Base64 prima di storage o trasporto.
Quest'ultimo punto è quello che confonde di più. Dati cifrati possono essere codificati in Base64, ma Base64 non è la cifratura. La sicurezza è arrivata da un'operazione crittografica come AES-GCM, XChaCha20-Poly1305, RSA-OAEP, JWE o TLS. Base64 ha solo reso i byte risultanti facili da salvare in JSON, email, header o colonna di database.
Usa parole precise nel codice e nei testi di prodotto:
encodedBackup // OK se è solo Base64
encryptedBackup // OK solo se c'è stata cifratura vera
signedState // OK se un HMAC o una firma proteggono l'integrità
passwordHash // OK se l'ha prodotto un algoritmo di hashing di password
Qui i nomi non sono cosmetica. Un nome fuorviante può far pensare a chi rileggerà il codice in futuro che ci sia un confine di sicurezza dove non c'è.
Codifica vs cifratura vs hashing vs firma
Queste operazioni risolvono problemi diversi:
| Operazione | Compito principale | Serve un segreto? | Reversibile? | Esempio |
|---|---|---|---|---|
| Codifica | Cambiare la rappresentazione | No | Sì | Base64, Base64url, URL encoding |
| Cifratura | Nascondere il testo in chiaro | Sì | Sì, con chiave | AES-GCM, XChaCha20-Poly1305, JWE |
| Hashing | Impronta a senso unico | No | No | SHA-256 per checksum |
| Hashing di password | Verifica lenta della password | Salt più parametri di costo | No | Argon2id, bcrypt, scrypt |
| Firma / MAC | Provare integrità e origine | Sì, o coppia di chiavi | No | HMAC, JWS, firma Ed25519 |
Base64 risponde alla domanda "questa sequenza di byte può viaggiare su un canale testuale?". Non risponde a "chi può leggerlo?", "chi l'ha modificato?" o "questo utente può accedere a quel record?".
Errore reale di sicurezza 1: chiavi API nascoste nel codice frontend
Questo pattern spunta in app React, estensioni del browser, temi WordPress e strumenti di amministrazione interni:
// La chiave arriva comunque a ogni browser.
const apiKey = atob("c2stbGl2ZS1hYmMxMjM0NTY3ODk=");
fetch("https://api.example.com/report", {
headers: { Authorization: `Bearer ${apiKey}` },
});
Base64 non aiuta perché il browser riceve sia il valore codificato che il decoder. Chiunque può aprire DevTools, ispezionare il bundle JavaScript, eseguire la stessa atob() e copiare la chiave.
Il fix è architetturale:
- I segreti veri stanno sul server.
- Alle chiavi lato browser dai scope stretti e restrizioni per dominio.
- Fai passare le chiamate sensibili dal tuo backend.
- Ruota ogni segreto che è già arrivato agli utenti.
- Dai per scontato che gli scanner di segreti sappiano decodificare stringhe simili a Base64.
Codificare una chiave può rendere la code review più difficile. Non rende la chiave più sicura.
Errore reale di sicurezza 2: password salvate come Base64
Questo è dannoso perché può restare zitto in un database per anni:
// Storage reversibile. Non farlo.
const storedPassword = btoa(password);
// Dopo:
const password = atob(storedPassword);
Se il database perde dati, ogni password torna subito. Non c'è nessun costo di cracking perché non c'è nulla da crackare.
Lo storage delle password deve essere a senso unico:
// Forma del flow, con una libreria di hashing di password.
const hash = await argon2.hash(password);
const ok = await argon2.verify(hash, candidatePassword);
Un normale sistema di login non dovrebbe mai avere bisogno di recuperare la password dell'utente. Deve solo verificare una password candidata contro un hash salvato.
Errore reale di sicurezza 3: Basic Auth scambiato per cifratura
L'header sembra abbastanza codificato da ingannare la gente:
Authorization: Basic dXNlcjpwYXNzd29yZA==
Decodifica in:
atob("dXNlcjpwYXNzd29yZA==");
// "user:password"
HTTP Basic Auth è accettabile solo se la richiesta è protetta da HTTPS. TLS cifra lo scambio HTTP in transito. Base64 si limita a impacchettare username:password in una stringa ASCII adatta agli header.
Su HTTP semplice, un osservatore di rete può leggere le credenziali. In log, reverse proxy, tracce APM e dump di supporto, un header Basic Auth va trattato come una password in chiaro.
Errore reale di sicurezza 4: payload JWT trattati come privati
Un JWT firmato normale ha tre sezioni Base64url:
header.payload.signature
Le prime due si decodificano in JSON. La terza è la firma.
eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0
Si decodifica in:
{
"sub": "1234",
"role": "admin"
}
Questo non vuol dire che il token sia falso. Un JWT firmato può essere a prova di manomissione e allo stesso tempo leggibile. La firma protegge l'integrità: dice al server se il token è stato emesso da una parte fidata e se header o payload sono cambiati. Non nasconde il payload.
Non mettere password, chiavi API, segreti di sessione, numeri di carta completi, dettagli medici o dati privati di profilo nel payload di un JWT firmato normale. Se i claim hanno bisogno di riservatezza, usa JWE o evita di mettere quei dati nel token.
Inoltre: decodificare non è verificare. Uno strumento per sviluppatori può mostrare i claim; un server deve comunque verificare firma, issuer, audience, scadenza, key ID, policy sull'algoritmo e regole di autorizzazione specifiche dell'applicazione prima di fidarsi.
Errore reale di sicurezza 5: parametri di URL che sembrano opachi
Le app legacy passano spesso lo stato così:
/profile?data=eyJ1c2VySWQiOjQyfQ==
Che decodifica in:
{
"userId": 42
}
Ne nascono due problemi. Chiunque può leggerlo e chiunque può modificarlo e ricodificarlo. Se /profile?data=... è l'unica cosa che decide quale record utente caricare, il bug è l'autorizzazione mancante.
Pattern più sicuri:
- Tratta i dati decodificati dall'URL come input non fidato.
- Ricontrolla i permessi sul server.
- Preferisci un riferimento corto e casuale lato server quando lo stato è sensibile.
- Usa un HMAC o una firma se il client deve portare stato a prova di manomissione.
- Tieni lo stato privato fuori dagli URL, perché gli URL finiscono copiati in log, cronologia del browser, analytics e screenshot di supporto.
Errore reale di sicurezza 6: export "cifrati" che sono solo codificati
Un altro odore da code review è una funzione così:
function exportEncryptedBackup(data) {
return btoa(JSON.stringify(data));
}
Il nome della funzione dice cifrato. Il codice dice codificato.
Se l'export deve essere solo un blob di testo portabile, rinominalo:
function exportBase64Backup(data) {
return btoa(JSON.stringify(data));
}
Se agli utenti viene detto che l'export è cifrato, usa vera cifratura autenticata e rendi esplicita la gestione delle chiavi. Vuol dire libreria crypto verificata, nonces/IV casuali, tag di autenticazione, generazione sicura delle chiavi e un piano per il loro storage. Base64 può ancora avvolgere il ciphertext dopo.
A cosa serve davvero Base64
Base64 è utile. L'errore è assegnargli un compito di sicurezza.
Usi buoni:
- Dati binari dentro JSON, XML, HTML o email.
- Allegati email MIME.
- Piccole
data:URI per immagini o font. - Formattazione dell'output crittografico per ciphertext, firme, nonces, salt e chiavi pubbliche.
- Segmenti di serializzazione compatta JWT e JOSE.
- Cursori di paginazione opachi dove la leggibilità è una questione di prodotto, non un confine di sicurezza.
- Debug di campi API, payload JWT e blob di configurazione.
La formulazione sicura è "codificato in Base64", non "cifrato". Quella singola parola evita una quantità sorprendente di malintesi in produzione.
Cosa usare al suo posto
Se sei ricorso a Base64 perché volevi sicurezza, scegli la primitiva giusta per il lavoro:
| Obiettivo | Usa invece |
|---|---|
| Nascondere dati a utenti o attaccanti | Cifratura autenticata come AES-GCM o XChaCha20-Poly1305 |
| Proteggere i dati in transito | HTTPS/TLS |
| Salvare password | Argon2id, bcrypt o scrypt con salt per password e costo tarato |
| Salvare chiavi API | Secrets manager, ambiente server, scope, log di audit e rotazione |
| Provare che un valore non è cambiato | HMAC o firma digitale |
| Impedire agli utenti di editare lo stato in URL | Autorizzazione lato server più stato firmato o riferimento di sessione lato server |
| Mantenere privati i claim di un JWT | JWE, o evita di mettere claim privati nel token |
| Infilare binari in JSON, email o header | Base64 va bene |
La parte difficile raramente è il nome dell'algoritmo. Sono threat modeling, gestione delle chiavi, rotazione, controllo di accesso e decidere chi debba poter vedere il testo in chiaro in primo luogo.
Una checklist pratica per il code review
Cerca gli usi di Base64:
rg "btoa\\(|atob\\(|base64|Buffer\\.from\\(.*base64|toString\\('base64'\\)"
Classifica ogni occorrenza:
- Trasporto: conversione byte-testo per JSON, MIME, data URI, PEM o storage. Di solito va bene.
- Formattazione dopo la crypto: ciphertext, nonce, firma, salt o chiave pubblica. Di solito va bene se la crypto è corretta.
- Interfaccia opaca: cursore, codice di invito o stato portato dal client. Controlla autorizzazione e protezione contro la manomissione.
- Nascondere segreti: chiave API, password, token, credenziali, claim privato o dati utente. Non va bene.
- Nomenclatura fuorviante: variabili o testi UI dicono cifrato, sicuro, protetto, segreto, mascherato o nascosto. Sistema il nome o l'implementazione.
Poi chiediti:
- Il valore codificato arriverebbe a un browser, un'app mobile, un utente, un raccoglitore di log, un proxy o uno strumento di supporto?
- C'è un decoder proprio accanto?
- Un server verifica i permessi dopo la decodifica?
- Il valore è firmato se il client può modificarlo?
- Se è un JWT, la firma viene verificata prima di qualsiasi decisione di fiducia?
- Se è una password, perché è reversibile?
Questo tiene la review coi piedi per terra. Un PNG in una data URI è noioso. Una chiave API di produzione in un bundle JavaScript no.
Risposta agli incidenti: se trovi "segreti" in Base64
Se scopri un segreto codificato in Base64 nel codice sorgente, nei log, in un ticket, nell'analytics, in un URL o in un export di database:
- Decodificalo in locale per confermare cos'è.
- Tratta il valore decodificato come esposto.
- Ruota credenziali, token e chiavi che potrebbero essere state visibili.
- Rimuovi la copia codificata da codice, log, screenshot o ticket dove possibile.
- Sostituisci il design con un segreto lato server, una chiave pubblica scoped, stato firmato, cifratura vera o hash della password.
- Aggiungi una regola di review perché lo stesso pattern non torni.
Non perdere tempo a discutere se la stringa Base64 fosse "difficile da notare". Scanner automatici, utenti del browser e attaccanti la sanno decodificare tutti.
Quando ti serve davvero un token cifrato
Un JWT firmato standard usa JWS: payload leggibile, firma a prova di manomissione. Se ti serve davvero la riservatezza dei claim, usa JWE (JSON Web Encryption).
Un JWE compatto ha cinque sezioni Base64url:
protected-header.encrypted-key.iv.ciphertext.authentication-tag
Quelle sezioni restano testo Base64url. La riservatezza viene dall'algoritmo di cifratura JWE e dalla chiave del destinatario, non da Base64url in sé.
In molte web app, un design più semplice funziona meglio: tieni lo stato sensibile sul server e manda al browser un identificatore di sessione corto e casuale. Non ogni JWT leggibile deve diventare un JWE.
Domande frequenti
Base64 è crittografia?
No. Base64 è una codifica reversibile senza chiave segreta. Chiunque abbia la stringa può decodificarla con una funzione del browser, un comando da terminale, uno strumento online o poche righe di codice.
Base64 è almeno offuscamento?
Solo nel senso più debole. Può nascondere il valore a un'occhiata veloce, ma non fermerà uno sviluppatore, un attaccante, uno scanner, un'estensione del browser, un log reader o uno strumento di supporto.
Base64 è sicuro per password o chiavi API?
No. Le password vogliono hashing lento come Argon2id, bcrypt o scrypt. Le chiavi API devono restare lato server o in un secrets manager con scope, auditabilità e rotazione.
Chiunque può leggere il payload di un JWT?
Sì, per i JWT firmati ordinari. Header e payload sono JSON codificato in Base64url. La firma può dimostrare l'integrità dopo la verifica, ma non nasconde i claim.
Perché Basic Auth usa Base64 se non è sicuro?
Basic Auth usa Base64 per far entrare username:password in un header HTTP. HTTPS/TLS fornisce la cifratura in transito. Senza HTTPS, le credenziali Basic Auth sono di fatto in chiaro sulla rete.
Base64url è più sicuro di Base64?
No. Base64url sostituisce + e / con - e _ e spesso omette il padding perché il valore funzioni in URL e JWT. Resta comunque codifica reversibile.
Cosa dovrei usare al posto di Base64 per cifrare?
Un design di cifratura autenticata verificato come AES-GCM, XChaCha20-Poly1305 o JWE, più generazione di chiavi, storage delle chiavi, rotazione e controllo di accesso a regola d'arte. Non progettare un tuo formato crittografico.
A cosa serve davvero Base64?
Base64 rappresenta byte come testo: campi binari in JSON, allegati email, data URI, file PEM, firme, formattazione di ciphertext e segmenti di token URL-safe. È un wrapper, non un confine di sicurezza.
Codifica e decodifica nel browser
Devi ispezionare una stringa Base64 subito? Base64 Encode & Decode su fixjson.org ti fa codificare o decodificare testo localmente nel browser, incluso il contenuto UTF-8. Usalo per ispezionare payload JWT, campi di risposta API o data: URI, ma non trattare la decodifica come verifica o decifratura.
Strumenti e guide correlati
- Base64 Encode & Decode — codifica e decodifica Base64 localmente nel browser.
- Decodifica stringhe Base64 e payload JWT — decodifica pratica di Base64 e Base64url.
- Come decodificare un JWT — leggere i claim e capire perché decodificare non è verificare.
- JSON sensibile e strumenti locali — decidere cosa è sicuro incollare in strumenti da browser.
- JSON Stringify — ispezionare ed escapare stringhe JSON annidate.
- RFC 4648: lo standard Base64 — il contesto formale di Base64 e Base64url.
- RFC 7519: JSON Web Token — struttura del JWT e considerazioni di sicurezza.
Fonti
- RFC 4648 — Base64, Base64url, padding, alfabeti e considerazioni di sicurezza.
- RFC 7617 — HTTP Basic Authentication e il suo formato di credenziali in Base64.
- RFC 7519 — struttura del JWT, decisioni di fiducia e considerazioni sulla privacy.
- RFC 7516 — JSON Web Encryption e serializzazione compatta JWE.
- MDN btoa e MDN atob — primitive Base64 del browser e insidie UTF-8.
- OWASP Cryptographic Storage Cheat Sheet — cifratura, modalità autenticate e gestione delle chiavi.
- OWASP Password Storage Cheat Sheet — linee guida sull'hashing delle password.
Ultima revisione luglio 2026.