Decodifica Base64 in JavaScript/Node.js: una guida completa
La tua applicazione riceve una stringa Base64. Potrebbe essere l'intestazione Authorization di una richiesta in arrivo, un campo dentro un carico utile JSON, un'immagine nascosta in una data URL, oppure un certificato incollato in un file di configurazione. Sono tutti la stessa cosa: byte grezzi in un costume da ASCII. Questo articolo parla di togliere quel costume in JavaScript e Node.js, e di farlo senza perdere nemmeno un byte lungo la strada.
Una parola sul formato in sé: il Base64 è una codifica testuale che mappa ogni tre byte in ingresso su quattro caratteri stampabili. La home page di questo sito spiega l'alfabeto, la matematica e il riempimento in ogni dettaglio, quindi qui restiamo in una sola frase. Una conseguenza vale la pena tenerla in tasca: i dati codificati sono circa il 33 percento più grandi dei byte che portano, il che significa che decodificare è un'operazione che rimpicciolisce, e nulla in questo articolo aggiunge o toglie segretezza. Stai aprendo un pacco, non rompendo un sigillo.
La buona notizia: non installi nulla. I browser distribuiscono atob() da vent'anni, Node.js ha la classe Buffer con una modalità base64 integrata, e i runtime moderni offrono ormai Uint8Array.fromBase64(), il nuovo arrivato rigoroso e configurabile della specifica ES2026. L'arte sta nel scegliere lo strumento giusto per il lavoro e nel sapere esattamente cosa perdona ciascuno, perché su un server decodifichi dati di sconosciuti, ed è la tolleranza il punto in cui le cose prendono una piega sbagliata.
Scegliere un decodificatore
Tre API coprono la grande maggioranza del lavoro di decodifica. Differiscono per temperamento, ed è quella differenza tutta la storia:
| Decodificatore | Disponibile in | Temperamento |
|---|---|---|
Buffer.from(string, 'base64') |
Node.js (ogni versione che conti) | Tollerante: salta i caratteri sconosciuti, si ferma al primo =, non lancia mai eccezioni |
atob(string) |
Tutti i browser, Node.js 16 e successivi | Rigido: lancia InvalidCharacterError su input sbagliati, salta gli spazi bianchi ASCII, perdona il riempimento mancante |
Uint8Array.fromBase64(string) |
Chrome 140+, Firefox 133+, Safari 18.2+, Node.js 25+ | Configurabile: scegli tu l'alfabeto e quanto deve essere rigoroso l'ultimo blocco |
Tutti e tre aprono lo stesso classico carico utile allo stesso modo:
// Il cavallo da lavoro di Node.js
const { Buffer } = require('node:buffer');
console.log(Buffer.from('aGVsbG8gd29ybGQ=', 'base64').toString('utf8')); // "hello world"
// La coppia di eredità (ogni browser, Node.js 16+)
console.log(atob('aGVsbG8gd29ybGQ=')); // "hello world", come stringa binaria
// Il metodo moderno ES2026 (Chrome 140+, Node.js 25+)
console.log(new TextDecoder().decode(Uint8Array.fromBase64('aGVsbG8gd29ybGQ='))); // "hello world"
Un'avvertenza prima di appoggiarti troppo a atob(): restituisce una stringa, ma una stringa binaria, una stringa in cui ogni carattere custodisce un byte grezzo come punto di codice da 0 a 255. Stamparne una va benissimo. Conservarla in JSON, in un database o in un cookie porta con sé quei valori di byte grezzi, quindi convertila subito in veri byte o in vero testo non appena hai decodificato.
Il decodificatore tollerante e ciò che inghiotte
Il Buffer di Node è un lettore indulgente, ed è una spada a doppio taglio. È fantastico per dati che hanno attraversato strade accidentate: email MIME con i loro a capo, stringhe copiate a mano, output di log con spazi fuori posto. È pericoloso per dati che non hai prodotto tu, perché non si lamenta mai. Ecco cosa succede davvero:
| Input | Cosa fa Buffer.from(input, 'base64') |
|---|---|
'!!!' |
Restituisce un Buffer vuoto. Tutta la spazzatura viene saltata, nulla viene decodificato, nessun errore. |
'aGVsbG8== garbage' |
Restituisce "hello". Il primo = termina la decodifica; il resto è ignorato. |
'aG!VsbG8' |
Restituisce "hello". Il punto esclamativo viene saltato, non è un errore. |
'aGVs=bG8' |
Restituisce "hel". Un = a metà stringa blocca lo spettacolo in anticipo. |
'aGVsbG8====' |
Restituisce "hello". Il riempimento finale in eccesso è ignorato. |
'=aGVsbG8' |
Restituisce un Buffer vuoto. Il riempimento prima dei dati non significa nulla. |
La soluzione per gli input non affidabili è un validatore, e la grammatica del Base64 è abbastanza piccola da stare in una sola espressione regolare:
const STRICT = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/;
function decodeStrict (base64) {
if (!STRICT.test(base64)) {
throw new TypeError('Not a valid base64 string');
}
return Buffer.from(base64, 'base64');
}
console.log(decodeStrict('aGVsbG8gd29ybGQ=').toString('utf8')); // "hello world"
try {
decodeStrict('aGVs!bG8');
} catch (error) {
console.log(error.message); // "Not a valid base64 string"
}
L'espressione regolare controlla la forma: gruppi di quattro con il riempimento corretto. Una regola che non può controllare è la regola di codifica canonica dell'RFC 4648, secondo cui i bit di riempimento non utilizzati dell'ultimo gruppo devono essere zero. La modalità rigorosa di Uint8Array.fromBase64() la controlla, quindi su Node.js 25 o su qualsiasi browser moderno puoi saltare del tutto l'espressione regolare e lasciare che sia la piattaforma a fare l'audit:
console.log(new TextDecoder().decode(Uint8Array.fromBase64('aGVsbG8'))); // "hello", la modalità loose perdona il riempimento mancante
try {
Uint8Array.fromBase64('QQB=', { lastChunkHandling: 'strict' });
} catch (error) {
console.log(error.name); // "SyntaxError", i bit di riempimento non sono zero
}
L'opzione lastChunkHandling ha tre impostazioni che vale la pena conoscere. "loose" (il predefinito) salta gli spazi bianchi, accetta il riempimento mancante e ignora i bit di riempimento rimanenti. "strict" esige un ultimo gruppo completo e riempito, con tutti i bit di riempimento a zero. E "stop-before-partial" decodifica solo gruppi completi di quattro caratteri e lascia il frammento finale a te da trasportare, ed è proprio la parte che rende piacevole la decodifica in streaming, come vedrai più avanti in questo articolo.
Dai byte al testo: la scelta della codifica dei caratteri
Decodificare Base64 ti consegna dei byte. I byte diventano testo solo quando scegli una codifica dei caratteri, e quella scelta spetta a te, di solito in base a ciò che il mittente ha promesso. Il predefinito di Node è quello che vuoi nella maggior parte dei casi:
const { Buffer } = require('node:buffer');
const bytes = Buffer.from('w6k=', 'base64'); // i due byte C3 A9
console.log(bytes.toString('utf8')); // "é", i due byte si uniscono in un carattere
console.log(bytes.toString('latin1')); // "é", gli stessi byte letti un carattere alla volta
C'è una piega con l'UTF-8: quando una sequenza di byte non è UTF-8 valido, Node non lancia eccezioni. Sostituisce il carattere di sostituzione Unicode (U+FFFD, il rombo con il punto interrogativo) e va avanti, il che significa che un carico utile corrotto può navigare indisturbato nel tuo flusso di lavoro fino al tuo database. Il vero decodificatore di testo della piattaforma, TextDecoder (una globale in Node.js e in ogni browser), ha un'opzione fatal che trasforma la corruzione in un TypeError che puoi catturare:
const stray = new Uint8Array([0xe9]); // un byte solitario, non UTF-8 valido
console.log(new TextDecoder().decode(stray)); // il carattere di sostituzione, nessun errore
try {
new TextDecoder('utf-8', { fatal: true }).decode(stray);
} catch (error) {
console.log(error.name); // "TypeError"
}
I sistemi di eredità non muoiono mai, e TextDecoder sa ancora come leggerli. Accetta l'intera tabella di etichette dello Standard di codifica del WHATWG, quindi un carico utile Base64 di un'applicazione Windows degli anni Novanta, di un mainframe giapponese o di uno specchio FTP d'epoca può ancora essere decodificato con etichette come 'windows-1250', 'shift_jis', 'euc-kr' o 'gb18030', tutte insensibili al maiuscolo e minuscolo. Un'etichetta merita un'avvertenza, perché è costata tempo vero di debug: la specifica associa 'iso-8859-1', 'latin1' e persino 'us-ascii' al decodificatore Windows-1252. Il byte 0x80, un carattere di controllo nel vero Latin-1, esce come il simbolo dell'euro:
console.log(new TextDecoder('iso-8859-1').decode(new Uint8Array([0x80]))); // "€", non il Latin-1 che avevi chiesto
// Per una lettura Latin-1 davvero byte per byte, usa il lato Buffer:
console.log(Buffer.from('gA==', 'base64').toString('latin1')); // il carattere di controllo grezzo 0x80
Se hai davvero bisogno di quella mappatura grezza, la codifica 'latin1' del Buffer (il cui alias di eredità 'binary' è, nelle parole della documentazione di Node, un nome molto fuorviante) mappa il byte N sul punto di codice N senza la deviazione per Windows. Per tutto ciò che è moderno, UTF-8 più fatal: true è la coppia sicura.
Aprire a forza un JWT
Il carico utile Base64 più comune di gran lunga che un servizio JavaScript decodifica è un JSON Web Token, la stringa xxxxx.yyyyy.zzzzz che viaggia nell'intestazione Authorization di metà del web. Secondo l'RFC 7515, un JWS compatto è fatto di tre parti separate da punti, e le prime due sono oggetti JSON codificati in base64url senza riempimento. Leggerli in Node.js non richiede cerimonie, perché la modalità base64url è una codifica di prima classe:
const { Buffer } = require('node:buffer');
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWRhIn0.JMjpmDdNzQZpTuUO1H33GJsj7nWhBu-qxkPD0GL2uaA';
const [head, body, signature] = token.split('.');
console.log(JSON.parse(Buffer.from(head, 'base64url').toString('utf8'))); // { alg: 'HS256', typ: 'JWT' }
console.log(JSON.parse(Buffer.from(body, 'base64url').toString('utf8'))); // { sub: '123', name: 'Ada' }
Detto abbastanza spesso, ma vale la pena ripeterlo: decodificare non è verificare. Intestazione e carico utile sono in abito, non cifrati, e chiunque possieda il token può leggerli entrambi. La parte che devi controllare è la terza, la firma. Per un token classico HMAC-SHA256 l'intero controllo sono poche righe del modulo crypto integrato, e l'unica parte sottile è confrontare con timingSafeEqual in modo che un attaccante non possa cronometrare il tuo confronto byte per byte:
const crypto = require('node:crypto');
const expected = crypto.createHmac('sha256', 'topsecret').update(head + '.' + body).digest();
const actual = Buffer.from(signature, 'base64url');
console.log(crypto.timingSafeEqual(expected, actual)); // true
console.log(crypto.timingSafeEqual(crypto.createHmac('sha256', 'wrong-secret').update(head + '.' + body).digest(), actual)); // false
In un servizio vero di solito non lo fai a mano. Il pacchetto jose (zero dipendenze, gira su Node.js, browser e runtime edge) e il collaudato pacchetto jsonwebtoken (Node.js) avvolgono la coreografia, gestiscono le famiglie di algoritmi RSA ed ECDSA e applicano i claim exp, aud e iss. Qualsiasi libreria tu scelga, l'impianto Base64 sottostante sono le stesse due chiamate che hai appena visto.
HTTP: intestazioni, stringhe di query e cookie
Tre angoli del cavo sono pieni di Base64. Il più vecchio è l'autenticazione HTTP Basic, definita nell'RFC 7617: il client invia Authorization: Basic più il Base64 di user-id:password. Sul server è un taglio e una decodifica, con il piccolo dettaglio di protocollo che solo il primo duepunti separa il nome utente dalla password, quindi una password può legalmente contenere altri duepunti mentre un nome utente no:
const { Buffer } = require('node:buffer');
const header = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==';
const credentials = Buffer.from(header.slice(6), 'base64').toString('utf8');
const [user, ...rest] = credentials.split(':');
console.log(user, rest.join(':')); // "Aladdin" "open sesame"
E ricorda cos'è davvero l'autenticazione Basic: oscuramento, non sicurezza. Le credenziali attraversano il cavo in costume, ed è per questo che lo schema è accettabile solo su HTTPS. Il secondo angolo è la stringa di query, e nasconde la mina più insidiosa del territorio Base64:
const params = new URLSearchParams('token=aGVs+bG8=');
console.log(params.get('token')); // "aGVs bG8=", il più è diventato uno spazio
Il tuo Base64 non si è corrotto da solo. L'ha fatto il livello URL, con cortesia, per conto delle regole di codifica dei moduli, che trattano + come uno spazio. È esattamente per questo che i token che vivono nelle stringhe di query usano l'alfabeto URL-safe, coperto nella sezione sotto. Il terzo angolo è il cookie: i cookie sono solo ASCII, quindi qualsiasi valore non ASCII conservato in uno di essi è quasi certamente Base64, e il vecchio schema di mettere un blob JSON in Base64 dentro un cookie è vivo in un numero sorprendente di sistemi di produzione. La decodifica è la stessa che già conosci; valida prima la forma, perché un cookie è il genere di posto dove un utente, o un'estensione del browser, può consegnarti della spazzatura.
File, immagini e data URL
Il filesystem di Node parla Base64 direttamente, quindi un intero file può attraversare un confine JSON in una sola riga:
const fs = require('node:fs');
const base64 = fs.readFileSync('./photo.png', 'base64');
console.log(base64.length); // il file, circa il 33 percento più pesante
const bytes = Buffer.from(base64, 'base64');
fs.writeFileSync('./photo.copy.png', bytes);
L'altro carico utile a forma di file è la data URL, la stringa data:image/png;base64,... che i front end amano per le immagini inline. La ricetta è la stessa in ogni runtime: taglia alla prima virgola, analizza i metadati che la precedono, decodifica il resto. Ecco un vero PNG di un pixel che torna in vita:
const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
const comma = dataUrl.indexOf(',');
const meta = dataUrl.slice(5, comma);
const bytes = Buffer.from(dataUrl.slice(comma + 1), 'base64');
console.log(meta); // "image/png;base64"
console.log(bytes.subarray(0, 8).toString('hex')); // "89504e470d0a1a0a", la firma del PNG
Controllare la firma è un'abitudine a basso costo. I primi otto byte di un PNG sono sempre 89 50 4E 47 0D 0A 1A 0A, e una JPEG inizia con FF D8 FF. Se un'"immagine base64" di un client non inizia con i byte magici che prometteva, lo sai adesso, prima di fare qualsiasi cosa di costoso con essa.
Base64 URL-safe: l'alfabeto per i token
Il Base64 classico usa + e / come i suoi due caratteri speciali (RFC 4648, sezione 4), e tutti e due sono guai negli URL: + diventa uno spazio durante la decodifica dei moduli, e / è un separatore di percorso. La variante sicura per URL e nomi di file della sezione 5, che tutti chiamano base64url, li scambia con - e _, e può eliminare del tutto il riempimento finale = quando la lunghezza è nota dal contesto. È esattamente la combinazione che servono a JWT, token OAuth e deep link, quindi base64url è l'alfabeto che incontrerai più spesso in giro.
Il Buffer di Node rende tutta la storia una non-notizia. Sia la modalità di decodifica 'base64' sia quella 'base64url' accettano tutti e quattro i caratteri speciali e li mappano sugli stessi valori, quindi una parte di JWT, un token OAuth e un blob Base64 classico si decodificano tutti senza alcuna cerimonia di scambio di caratteri:
const { Buffer } = require('node:buffer');
console.log(Buffer.from('aGVs-bG8', 'base64').toString('hex')); // "68656cf9b1bc"
console.log(Buffer.from('aGVs+bG8', 'base64url').toString('hex')); // "68656cf9b1bc", gli stessi identici sei byte
L'API ES2026 è pignola apposta, e ti dà la stessa flessibilità con una manopola esplicita. L'opzione alphabet seleziona tra "base64" (il predefinito, + e /) e "base64url" (- e _), e dargli un carattere dell'alfabeto sbagliato è un SyntaxError, non una decodifica silenziosa tra un alfabeto e l'altro:
console.log(Uint8Array.fromBase64('aGVs-bG8', { alphabet: 'base64url' }).length); // 6
try {
Uint8Array.fromBase64('aGVs-bG8'); // l'alfabeto predefinito è quello classico
} catch (error) {
console.log(error.name); // "SyntaxError", il trattino non è un carattere classico
}
In un browser che non ha ancora i nuovi metodi, la deviazione è un piccolo scambio prima di passare la stringa a atob(), che conosce solo l'alfabeto classico. Devi anche ripristinare il riempimento se il mittente l'ha fatto cadere, ed è la norma per i carichi utili di tipo token:
function decodeBase64Url (value) {
const classic = value.replace(/-/g, '+').replace(/_/g, '/');
const padded = classic + '='.repeat((4 - (classic.length % 4)) % 4);
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
console.log(new TextDecoder().decode(decodeBase64Url('aGVsbG8gd29ybGQ'))); // "hello world"
Base64 in natura: dove si nascondono i carichi utili
Il Base64 è il servizio postale dei byte nel mondo JavaScript. Un tour dei posti dove appare, con la ricetta di decodifica per ogni tappa:
- Campi delle API JSON, di gran lunga il vettore più comune: avatar, miniature, documenti generati e caricamenti arrivano come stringhe Base64 dentro JSON ordinari, perché il JSON non ha una parola per "questi sono byte". Decodifica il campo prima di farci qualsiasi altra cosa.
- Variabili d'ambiente e file di configurazione: diversi gestori di segreti, sistemi CI e persino la CLI di npm ti consegnano blob Base64 (le versioni npm più vecchie conservavano le credenziali del registro in
.npmrccome Base64 diuser:password; l'npm moderno scrive un token bearer grezzo in_authToken). Decodifica una volta all'avvio, e tieni il testo in chiaro in memoria solo finché te ne serve. - Kubernetes e strumenti di cluster: i segreti k8s sono famosi per essere codificati in Base64 nell'API e in
etcd, e i documenti ufficiali continuano a ripetere che si tratta di codifica, non di cifratura. Il tuo codice di decodifica dovrebbe trattare il risultato come un segreto, non come una prova di sicurezza. - Database: qualsiasi cosa binaria conservata in una colonna JSON (Postgres
jsonb, documenti MongoDB, Redis) è frequentemente una stringa Base64. Decodificala sul percorso di lettura in un Buffer o un Uint8Array, e lascia che il database resti solo testo. - Email: il Base64 MIME con il suo a capo a 76 caratteri è il modo in cui allegati e intestazioni binarie attraversano SMTP, un protocollo che originariamente era solo a 7 bit. Il decodificatore di Node salta gli a capo per te, quindi l'intero corpo si decodifica in una chiamata senza pulizia.
- Pipeline CI e CD: i sistemi di build e gli iniettori di segreti passano token come valori di variabili d'ambiente in Base64; decodifica nello script della pipeline, e non riversare mai il valore decodificato in un log.
- Dati directory e SAML: i file LDIF conservano attributi binari (pensa: certificati) in Base64, e le risposte SAML sono spesso compresse con deflate e poi codificate in Base64 prima di attraversare un confine HTTP.
- Worker threads e runtime edge: le stringhe Base64 attraversano il confine
worker_threadscome semplici stringhe clonabili con structured clone, quindi una decodifica pesante può vivere su un worker mentre l'event loop del thread principale resta libero.
Due di quelle tappe meritano un'occhiata da vicino, perché compaiono sia nei colloqui di lavoro sia in produzione:
const { Buffer } = require('node:buffer');
// Variabile d'ambiente: il segreto arriva codificato in Base64
const token = Buffer.from(process.env.REGISTRY_TOKEN_B64, 'base64').toString('utf8');
// Campo di API JSON: apri il pacco prima di fare qualsiasi altra cosa
const body = { attachment: 'iVBORw0KGgo...' };
const imageBytes = Buffer.from(body.attachment, 'base64');
console.log(imageBytes.subarray(0, 4).toString('hex')); // "89504e47", di nuovo la firma del PNG
// Email MIME: gli a capo vengono saltati, nessuna pulizia necessaria
const mimeBody = 'SGVsbG8sIHdyYXBw\nZWQgYmFzZTY0IQ==';
console.log(Buffer.from(mimeBody, 'base64').toString('utf8')); // "Hello, wrapped base64!"
La pratica errata da riconoscere in questo tour è sempre la stessa: Base64 in un posto dove i byte grezzi erano già ammessi. Un frame WebSocket, un flusso di file, una colonna bytea di Postgres, tutti portano byte in modo nativo, quindi un giro di Base64 lì è puro sovraccarico, la tassa del 33 percento di dimensione senza alcun beneficio da mostrare. Quando esiste un percorso binario nativo, prendilo.
Decodificare a pezzi: flussi e dati grossi
I gruppi di quattro caratteri del Base64 codificano tre byte, quindi un flusso di frammenti può spezzare un gruppo a metà. L'approccio ingenuo, decodifica ogni frammento e prega, corrompe l'output a confini casuali. L'API ES2026 è stata progettata esattamente per questo: setFromBase64() scrive in un array già allocato e riferisce quanti caratteri in ingresso ha consumato, e la modalità "stop-before-partial" lo fa fermare all'ultimo gruppo completo, lasciando il frammento al frammento successivo. Il schema ricalca l'API di flusso di TextDecoder:
const { Buffer } = require('node:buffer');
const chunks = ['aGVsbG8', 'gd29ybGQ='];
let leftover = '';
const parts = [];
for (const chunk of chunks) {
const pending = leftover + chunk;
const space = new Uint8Array(Math.ceil(pending.length * 3 / 4));
const { read, written } = space.setFromBase64(pending, { lastChunkHandling: 'stop-before-partial' });
parts.push(Buffer.from(space.buffer, space.byteOffset, written));
leftover = pending.slice(read);
}
parts.push(Buffer.from(Uint8Array.fromBase64(leftover)));
console.log(Buffer.concat(parts).toString('utf8')); // "hello world"
Sui runtime senza i nuovi metodi (e la linea LTS di Node non li aveva per un bel po'), lo stesso ciclo funziona con un piccolo decodificatore di livello utente che tiene traccia di un gruppo parziale, oppure basta accumulare i frammenti in arrivo finché non puoi tagliare sui confini dei gruppi. L'idea importante è trascinare il resto: non decodificare mai un frammento da solo.
I carichi utili grossi attirano la tua attenzione su altri due limiti. Primo, la stringa stessa: buffer.constants.MAX_STRING_LENGTH di Node è 536870888 caratteri, circa 512 MiB di testo, che decodificati diventano circa 400 MB di byte. Un "file base64" più grande di quello richiede un approccio in streaming, non un singolo readFileSync. Secondo, la memoria: la stringa codificata vive nell'heap JavaScript come UTF-16, due byte per carattere, e il Buffer decodificato è una seconda copia dei dati. Per i carichi utili grandi li tieni brevemente entrambi in mano, quindi tieni in vita la forma codificata il meno a lungo possibile per il codice, e preferisci i flussi per qualsiasi cosa di dimensioni di file.
Dalla terminale
Node fa anche da decodificatore Base64 da riga di comando perfettamente onesto, il che è comodo quando stai facendo debug di una richiesta o ispezionando un valore di configurazione:
# Decodifica una stringa Base64 classica passata come argomento
node -e 'console.log(Buffer.from(process.argv[1], "base64").toString("utf8"))' "aGVsbG8gd29ybGQ="
# La variante URL-safe, riempimento opzionale
node -e 'console.log(Buffer.from(process.argv[1], "base64url").toString("utf8"))' "aGVsbG8gd29ybGQ"
# Decodifica da stdin, a questo servono i tubi
echo -n "aGVsbG8gd29ybGQ=" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>console.log(Buffer.from(d.trim(),"base64").toString("utf8")))'
Tutti e tre stampano hello world. Se sulla macchina hai anche il classico comando base64 di coreutils, fa lo stesso lavoro con base64 -d, ma le versioni di Node conoscono base64url, cosa che lo strumento tradizionale non fa.
Trappole con l'accento JavaScript
Ognuna di queste è stata un pomeriggio perso di qualcuno in JavaScript o Node.js:
- Il decodificatore silenzioso:
Buffer.from('!!!', 'base64')restituisce un Buffer vuoto, non un errore. Un input mezzo corrotto si decodifica in dati mezzo corrotti senza alcun avviso. Validati gli input non affidabili con l'espressione regolare rigorosa (o la modalità rigorosa difromBase64), e tratta un Buffer vuoto ottenuto da una stringa non vuota come un segnale rosso. - L'argomento codifica mancante:
Buffer.from('aGVsbG8=')senza secondo argomento non decodifica nulla. Costruisce un Buffer dai byte UTF-8 di quelle lettere, quindi i tuoi dati "decodificati" sono le lettere stesse, rimessi in pacco come byte. L'argomento'base64'è tutto l'ingegno. - Il costume da stringa binaria: l'output di
atob()non è testo finché non dici che lo è. Infilarlo in una risposta JSON, in un cookie o in una riga di log "funziona", e conserva anche ogni byte nullo, il che sorprende i servizi di invio dei log e i serializzatori in egual misura. Converti concharCodeAt()in un Uint8Array, o in testo UTF-8, immediatamente. - Il più nella stringa di query: un
+in un valore di query decodificato da modulo è già uno spazio quandoURLSearchParamste lo consegna. Preferisci base64url per qualsiasi cosa che viva in un URL, e non incollare mai un token Base64 classico non codificato in una stringa di query. - Il carattere di sostituzione: l'UTF-8 invalido diventa un rombo-punto-interrogativo silenzioso nella modalità UTF-8 del Buffer invece di un errore, quindi un carico utile corrotto può passare nel tuo flusso e finire in un database. Attiva
fatal: trueconTextDecoderdove la corruzione dovrebbe essere un fallimento rumoroso. - La deviazione per Windows: chiedere a
TextDecoder'iso-8859-1'o'latin1'ti dà il decodificatore Windows-1252, dove il byte 0x80 diventa il simbolo dell'euro. Per un vero Latin-1 byte per byte, leggi il Buffer contoString('latin1')invece. E ricorda che'binary'è solo un alias fuorviante per la stessa mappatura Latin-1. - I tetti di dimensione:
buffer.constants.MAX_LENGTHè 9007199254740991 byte (2 alla 53, meno uno) sui sistemi a 64 bit, ma la stringa che porta il Base64 non può crescere oltreMAX_STRING_LENGTHdi 536870888 caratteri. Una singola stringa può quindi portare solo poco più di 400 MB di dati decodificati; oltre, fallo scorrere in streaming. - Il conto della memoria: una stringa Base64 costa due byte di heap per carattere (UTF-16), e il Buffer decodificato è una piena seconda copia. Un file da 100 MB diventa, per un breve momento nel tuo processo, circa 133 MB di stringa più 100 MB di Buffer. Riduci la finestra in cui la forma codificata resta riferita.
- Il disallineamento rigoroso da remoto, tollerante in locale: il tuo decodificatore Node perdona ciò che un decodificatore rigoroso altrove rifiuta (uno script Python, un servizio Go, un'app mobile). Se un lato del tuo sistema è rigoroso e l'altro tollerante, il bug appare solo su certe lunghezze del carico utile, il che è la peggior specie di bug. Accordatevi sul rigore a livello di protocollo, non in testa.
Come JavaScript ha fatto crescere i suoi decodificatori
Il lato browser ha una storia lunga, noiosa e affidabile. atob() e btoa() sono stati specificati nella bozza HTML5 all'inizio del 2011 (i browser li avevano già prima della specifica), e da allora stanno in ogni browser importante, senza cambiare comportamento da oltre un decennio. Precedono gli array tipizzati nello standard del linguaggio (ES2015), ed è per questo che parlano in "stringhe binarie" invece che in byte.
Node.js ha fatto crescere il suo decodificatore su un'altra linea temporale. La classe Buffer è diventata una globale nella versione 0.1.103, nell'estate del 2010, quasi cinque anni prima del Node 1.0, e portava la modalità 'base64' fin dall'inizio. Per gran parte della vita di Node è stato l'unico decodificatore in città. Poi è arrivata l'ondata degli standard web: Node 16 nel 2021 ha aggiunto atob() e btoa() come globali in modo che il codice scritto per il browser girasse sul server senza un polyfill, e ha marchiato entrambi come Legacy fin dal primo giorno. Node 25, rilasciato il 15 ottobre 2025, ha aggiornato V8 a 14.1 e ha portato nel runtime i metodi ES2026, Uint8Array.fromBase64(), setFromBase64() e i loro fratelli in esadecimale. Lungo la strada, il vecchio costruttore new Buffer() è stato deprecato (Node 10 ha iniziato gli avvisi nel 2018) in favore di Buffer.from(), alloc() e allocUnsafe(), in parte perché un'allocazione non inizializzata poteva perdere qualsiasi memoria ci fosse stata prima.
Nei browser la stessa ondata è approdata leggermente prima: Firefox 133 e Safari 18.2 hanno distribuito i nuovi metodi nel 2024, e Chrome 140 (stabile dal 2 settembre 2025) ha completato il set, a quel punto la funzione è stata dichiarata Baseline Newly available nel programma Baseline dei fornitori di browser. Bun, il runtime JavaScript tutto-in-uno, li ha ottenuti nella versione 1.1.22 nell'agosto 2024. E se non puoi richiedere un runtime recente, core-js e il pacchetto es-arraybuffer-base64 del progetto es-shims distribuiscono polyfill per tutto, che è anche la strada che la maggior parte dei framework segue internamente.
Il formato che servono ha una genealogia ancora più vecchia. L'alfabeto è stato standardizzato per la prima volta per la Privacy-Enhanced Mail nel 1987 (RFC 989), la revisione del 1993 (RFC 1421) ha mantenuto lo stesso alfabeto, e il MIME se l'è preso nel 1996 (RFC 2045), circa tre anni dopo quella revisione, con il suo a capo a 76 caratteri; l'RFC 3548 nel 2003 ha consolidato base16, base32 e base64 in un unico documento, e l'RFC 4648 nel 2006 lo ha ripubblicato, mantenendo l'alfabeto URL-safe che l'RFC 3548 aveva aggiunto - quello che, un decennio dopo, sarebbe finito in ogni JWT. La variante URL-safe è un bel pezzo di cultura generale: fu proposta in un post del 2001 su una mailing list sugli identificatori peer-to-peer, prima ancora di incontrare un token.
Curiosità per la tua prossima riunione di inizio giornata
- La chiave di esempio dell'RFC di WebSocket,
dGhlIHNhbXBsZSBub25jZQ==, si decodifica nelle parole "the sample nonce". Il comitato di standardizzazione ha nascosto un battito di ciglia nel suo stesso esempio, e l'atob()di Node coglie la battuta in una chiamata. Buffer.from('!!!', 'base64')restituisce un Buffer di lunghezza zero. Una vera allocazione con niente dentro. Niente. È la cosa più vicina a un'alzata di spalle a cui Node riesce ad arrivare.- I decodificatori Base64 di Node sono bilingui in un modo che la specifica non ha mai chiesto:
+,-,/e_sono tutti i benvenuti sia nella modalità'base64'sia in quella'base64url', con ciascuna coppia mappata sullo stesso valore. - La documentazione di Node per
atob()contiene la frase "Usa Buffer.from(data, 'base64') invece". Un runtime che ti dice di smettere di usare una delle sue stesse globali, completo di un codemod ufficiale (npx codemod@latest @nodejs/buffer-atob-btoa) per fare la migrazione al posto tuo. - I piccoli Buffer sono ricavati da una lastra condivisa:
Buffer.poolSizeè 65536 byte, e ogni piccola allocazione riusa i pezzi di quel pool. È per questo che la creazione di Buffer è veloce, e per questo che l'allocazione "unsafe" è un'espressione di cui dovresti conoscere il significato. - Il piccolo pacchetto
base64-js, tre funzioni e zero dipendenze, incassa ben oltre 100 milioni di download a settimana su npm, quasi tutti come dipendenza nascosta dentro altri pacchetti. Il Base64 è il codice più contrabbandato dell'ecosistema. Uint8Array.fromBase64()ha una modalità chiamata"stop-before-partial"che esiste puramente perché tu possa decodificare un flusso senza mai spezzare un gruppo di quattro caratteri. Una modalità che porta il nome della cosa che si rifiuta di fare è una rara poesia di API.- Il mondo delle password Unix usa i propri alfabeti dal sapore Base64, senza riempimento, e in modo confuso, non sono tutti nello stesso ordine. L'alfabeto "hash64" del classico
crypt(3)è./0-9A-Za-z, ma bcrypt mescola gli stessi 64 caratteri in./A-Za-z0-9invece. Incontrerai la versione bcrypt negli hash$2b$che molti progetti JavaScript conservano per le password degli utenti, ed è la ragione per cui "base64" in un contesto di sicurezza può significare diversi alfabeti diversi, non solo due.
Manca una direzione
Decodificare Base64 in JavaScript e Node.js è una pila di tre strumenti onesti: Buffer.from(string, 'base64'), il cavallo da lavoro indulgente che accetta entrambi gli alfabeti e salta ogni carattere fuori posto, meglio se sorvegliato da un'espressione regolare rigorosa; TextDecoder, per il vero testo in qualsiasi codifica dei caratteri che il vecchio web abbia mai inventato, con la modalità fatal quando la corruzione dovrebbe far male; e il nuovo Uint8Array.fromBase64(), per il codice che mette i byte al primo posto e vuole alfabeti rigorosi, bit di riempimento rigorosi e streaming senza acrobazie. Fissa la codifica dei caratteri, valida ciò che gli sconosciuti ti mandano, confronta le firme con timingSafeEqual, e il formato smette di essere un mistero da entrambe le parti del divario browser/server.
E quando hai finito di aprire i pacchi, ricorda che qualcuno li ha dovuti sigillare. Il lato codifica ha le sue trappole: il muro Unicode che ferma btoa() a metà frase, l'avvolgimento di riga del MIME, le regole di riempimento del base64url, e il nuovo Uint8Array.toBase64() con la sua opzione omitPadding. Quella storia, con esempi di codice per ogni passo, è trattata in profondità nell'articolo correlato sulla codifica Base64 sul nostro sito sorella. Leggilo dopo, perché le trappole sono diverse e più divertenti su quel lato dell'alfabeto.
Ultimo aggiornamento: 2026-09-07
Articolo correlato: Codifica Base64 in JavaScript/Node.js: una guida completa