Base64 e codifica URL: scopo, insidie, uso corretto
Riepilogo (TL;DR)
Esegui echo -n "Hi" | base64 e ottieni esattamente quattro caratteri indietro: SGk=. Due byte in input (16 bit) vengono tagliati in blocchi da 6 bit, riempiti fino a un output di 4 caratteri, e questa è l’intera aritmetica. Trattare quella semplice regola come un default di “avvolgi tutto per sicurezza” e il costo si compone in fretta – tre byte in input diventano sempre quattro caratteri in output, che è circa il 33% di inflazione, e un JPEG da 1,2 MB che una volta ho visto un servizio avvolgere in Base64 dentro un campo JSON è atterrato a circa 1,64 MB sul filo. La codifica non è cifratura, e non è compressione. È un insieme di regole per esprimere un alfabeto dentro un altro così che i dati sopravvivano a un canale che altrimenti li mangerebbe. Base64 (RFC 4648 §4) esiste per spedire byte arbitrari attraverso sistemi che capiscono solo testo – corpi email, campi stringa JSON, dati immagine inline. Base64URL (la stessa RFC, §5) scambia due caratteri (+→-, /→_) così che il risultato sia sicuro da mettere in un path URL, un nome file o un JWT. Il percent-encoding (RFC 3986) è uno strumento separato: converte caratteri che significano qualcosa per gli URL (?, &, #, spazio, Unicode) in sequenze %XX che non lo fanno. Scegli la codifica che corrisponde al canale: binario se il percorso lo supporta, Base64 per canali testuali, Base64URL per dati embedded in URL, e percent-encoding per qualsiasi cosa già in un contesto URL. Le stringhe Base64 rivelate restano leggibili a chiunque abbia un decoder, quindi non trattare mai la codifica come un meccanismo di segretezza.
Contesto e concetti
I canali solo testo esistevano molto prima che qualcuno chiedesse loro di trasportare immagini. Storicamente l’email assumeva ASCII a 7 bit e rimuoveva o corrompeva byte con il bit alto impostato; le intestazioni HTTP restringono ancora certi caratteri; i campi stringa JSON sono UTF-8 ma non possono contenere in sicurezza byte di controllo grezzi (0x00–0x1F). Base64, standardizzato in RFC 4648, risolve il problema mappando ogni 3 byte di input (24 bit) su 4 caratteri di output di 6 bit ciascuno. L’alfabeto è 64 caratteri: A-Z (indici 0–25), a-z (26–51), 0-9 (52–61), + (62), / (63), con = come padding per mantenere la lunghezza dell’output multipla di quattro. Un byte di input produce XX== (8 bit → 6+2 con padding), due byte producono XXX= (16 bit → 6+6+4 con padding), e tre byte producono XXXX senza padding.
La variante URL-safe di Base64 cambia solo gli indici alfabetici 62 e 63 – + diventa -, / diventa _ – perché + decodifica come spazio nelle vecchie sottomissioni di form e / è un separatore di path. In molti contesti URL-safe il padding = viene anche eliminato; questa è la forma usata nei JSON Web Token (RFC 7519), dove una tripla compatta header.payload.signature deve entrare in un header Authorization: Bearer ... senza ri-escaping.
Il percent-encoding (a volte chiamato URL-encoding) è separato. RFC 3986 definisce due categorie di ASCII: caratteri unreserved (A-Z, a-z, 0-9, -, ., _, ~) che possono apparire alla lettera, e caratteri reserved (:, /, ?, #, [, ], @, !, $, &, ', (, ), *, +, ,, ;, =) che significano qualcosa strutturalmente e devono essere codificati quando appaiono come dati invece che come sintassi. Qualsiasi byte fuori da quell’insieme – inclusi i byte di un carattere non-ASCII codificato UTF-8 – viene scritto come %XX in esadecimale. encodeURIComponent in JavaScript codifica aggressivamente sia caratteri reserved che non-reserved per l’uso dentro un componente; encodeURI è più permissivo perché assume che stai passando un intero URL.
Una conseguenza sottile di queste tre codifiche è che non sono intercambiabili, anche quando i byte che producono per caso si sovrappongono. Un parametro di query percent-encoded è ancora caratteri ASCII che codificano altri caratteri ASCII; passarlo attraverso un decoder Base64 produce spazzatura. Una stringa Base64 standard messa in un path URL si rompe silenziosamente perché il / al suo interno viene letto come separatore di path – ho visto un collega perdere mezza giornata su un singolo carattere del genere. Queste confusioni sono la fonte di metà dei bug di codifica in produzione.
Confronto e dati
| Proprietà | Base64 (standard) | Base64URL | Percent-encoding |
|---|---|---|---|
| Quando usarlo | Canale testo che rifiuta binario grezzo (MIME email, stringa JSON di byte) | Lo stesso, ma embedded in un URL, nome file o JWT | Un singolo componente URL contenente caratteri reserved o non-ASCII |
| Alfabeto | A–Z a–z 0–9 + / = | A–Z a–z 0–9 - _ (padding opzionale) | Insieme unreserved; tutto il resto diventa %XX |
| Overhead | Circa 33% (4 caratteri per 3 byte), più line-wrapping a 76 caratteri in MIME | Circa 33%, nessun padding nell’uso tipico | Varia – 1 byte di UTF-8 diventa 3 byte ASCII (%XX) |
| Si rompe su | + e / grezzi negli URL, lunghezza senza padding in decoder stretti | Decoder Base64 standard che richiedono +/= | Double-encoding (dati già percent-encoded vengono ricodificati) |
| Usi comuni | Allegati MIME, data URI, binari inline in JSON | JWT, ID URL-safe, link brevi | Parametri query, segmenti di path, body di form |
I numeri contano per il budgeting. Un’immagine da 1 MB codificata in Base64 diventa circa 1,37 MB; con il line-wrapping a 76 caratteri di MIME, aggiungi un altro 2% circa. In una risposta HTTP quell’inflazione di dimensione colpisce sia il server che il parser del client. Il percent-encoding è di solito un problema minore a livello di stringa ma può moltiplicare i byte per testo CJK: un carattere coreano in UTF-8 occupa 3 byte, che diventano 9 caratteri ASCII dopo la codifica. La parola di due caratteri “안녕” diventa la sequenza da 18 byte %EC%95%88%EB%85%95 dentro un URL.
Scenari reali
Scenario 1 — Allegati email. Un PDF viaggia dentro una parte MIME con Content-Transfer-Encoding: base64. Il client di posta codifica il file in Base64, avvolge le righe a 76 caratteri (il classico limite MIME), e lo invia. Il destinatario inverte il processo. L’assunzione solo-testo di SMTP ha fatto di Base64 il default qui molto prima che esistessero estensioni moderne come 8BITMIME o BINARYMIME, e la maggior parte dei mail server emette ancora Base64 per sicurezza. L’overhead del 33% è accettato come costo di consegna affidabile.
Scenario 2 — JSON Web Token. Un JWT (RFC 7519) è header.payload.signature, dove ogni segmento è JSON o byte codificati in Base64URL, senza padding. L’alfabeto URL-safe significa che i token possono apparire in header Authorization, parametri query access_token e righe di log senza ri-escaping. L’assenza di padding li tiene brevi. Un avviso: chiunque decodifichi un JWT può leggere le claim – la codifica non è sicurezza; la firma HMAC o RSA lo è. Non mettere segreti nel payload.
Scenario 3 — Data URI per piccole immagini. Un background-image: url("data:image/png;base64,iVBORw0K...") inserisce un PNG direttamente nel CSS. Questo evita un round-trip per asset minuscoli come icone. Ma l’overhead del 33% di Base64 e la perdita di caching del browser e richieste parallele significano che è una vittoria solo per asset abbastanza piccoli che il round-trip HTTP extra sarebbe costato di più. Nella mia esperienza il punto di pareggio è circa 1–2 KB; sopra, un file cacheato separato o un SVG è di solito più veloce.
Scenario 4 — Upload di un avatar utente. Un anti-pattern comune è leggere un file con FileReader.readAsDataURL, che restituisce una stringa data:image/png;base64,..., e poi fare POST di quella stringa come campo JSON. Funziona, ma non è quasi mai la forma giusta: il payload è ora 33% più grande, il server deve decodificarlo prima di scrivere su disco, ed entrambi i lati bruciano memoria sulla forma stringa. In un caso che ho osservato, un’immagine da 5 MB si è gonfiata a circa 6,7 MB di JSON e ha causato timeout su reti mobili; passare a multipart/form-data ha spedito lo stesso file a 5 MB. Ricorri a Base64 in questo flusso solo quando il layer di trasporto richiede davvero una stringa JSON, come quando un’API di terze parti non accetta multipart.
Errori comuni
“Base64 è cifratura.” Non lo è. La mappatura è pubblicata in RFC 4648, l’alfabeto è fisso, e qualsiasi decoder restituisce i byte originali. La codifica protegge il trasporto, non il contenuto. Se il payload è sensibile, cifra prima e fai Base64 del ciphertext per il trasporto.
“La codifica URL serve solo per caratteri non-ASCII.” Molti caratteri ASCII reserved devono anch’essi essere codificati quando appaiono come dati. Un valore di query contenente & deve diventare %26, o il server lo parsera come nuovo parametro. # deve diventare %23, o tutto dopo viene trattato come frammento. La regola è strutturale, non basata sul set di caratteri.
“Tutto ciò che passa via HTTP dovrebbe essere codificato Base64 per sicurezza.” HTTP trasporta felicemente body binari – Content-Type: application/octet-stream, transfer chunked, qualsiasi valore di byte. Base64 è un workaround per canali che non lo fanno, e pagare un overhead del 33% quando il canale gestisce già i byte è tassa pura. Usa multipart/form-data o un body grezzo per upload di file, e riserva Base64 per i casi in cui il canale ha davvero bisogno di testo (un campo JSON, un parametro URL, un body MIME).
“Base64 e Base64URL sono intercambiabili.” Non lo sono. Passare una stringa URL-safe a un decoder Base64 stretto che si aspetta +/ fallirà o produrrà spazzatura. Le librerie di solito forniscono entrambi; abbina encoder e decoder end-to-end. Il Buffer.from(s, 'base64') di Node accetta entrambi gli alfabeti, ma non tutte le librerie standard sono così tolleranti.
Lista di controllo
- Il canale è solo 7-bit o testo strutturato? Body email, stringa JSON di byte, URI
data:CSS → Base64. - Il valore codificato va in un URL, nome file o JWT? Usa Base64URL e decidi se il padding è ammesso.
- Il valore è già un componente URL? Percent-encode, e solo le parti che ne hanno bisogno.
- I dati sono grandi? Considera se il canale richieda davvero la codifica – un body binario grezzo evita il 33% di overhead.
- La decodifica è stretta o permissiva? Abbina gli alfabeti standard/URL-safe end-to-end; non mischiare.
- La confidenzialità è un obiettivo? Cifra prima di codificare. La codifica da sola non rende mai i dati segreti.
Strumento correlato
L’encoder/decoder Base64 di Patrache Studio gestisce sia varianti standard che URL-safe localmente, così i byte in input non lasciano il tuo browser – utile quando i dati sono un token o una piccola chiave. Se il payload Base64 è per caso un JWT, abbinalo a Formattazione, validazione e schema di JSON nella pratica per ispezionare pulitamente le claim decodificate. E se il blob codificato è un ID compatto derivato da un UUID, UUID v1 vs v4 vs v7: scegliere una chiave primaria DB spiega perché i byte originali che hai codificato in Base64URL contano ancora per ordine di sort e indicizzazione.
Riferimenti
- IETF RFC 4648, “The Base16, Base32, and Base64 Data Encodings” — https://datatracker.ietf.org/doc/html/rfc4648
- IETF RFC 3986, “Uniform Resource Identifier (URI): Generic Syntax” — https://datatracker.ietf.org/doc/html/rfc3986
- MDN, “Base64” glossary entry — https://developer.mozilla.org/en-US/docs/Glossary/Base64
- MDN, “encodeURIComponent()” — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent