Decodifica Base64 in PHP: una guida completa
Viene fuori in un ticket di supporto, in un log di API, in un file di configurazione o in mezzo a un URL: una lunga stringa di lettere, cifre, ogni tanto un + o un /, e magari un = o due alla fine. Lo riconosci in un batter d'occhio. Base64 è un formato da binario a testo: riscrive ogni tre byte di dati grezzi in quattro caratteri tratti da un alfabeto di 64 lettere, e un paio di segni = chiudono la coda quando il numero di byte non è un multiplo di tre. La decodifica è la direzione che rimpicciolisce di questo scambio: quattro caratteri rientrano, tre byte escono. La home page di questo sito spiega il formato passo per passo, quindi questo articolo spende la sua energia dove spetta: sul lato PHP del lavoro.
Prima la notizia principale. PHP spedisce un decoder Base64 nel proprio core dal PHP 4. base64_decode() non richiede estensioni, pacchetti Composer né configurazione, e gira ovunque gira PHP. La notizia meno buona: il suo umore predefinito inghiotte in silenzio gli input corrotti e ti restituisce dati spazzatura senza una parola. La buona notizia diventa ancora migliore: un'unica opzione ($strict) trasforma la funzione in un vero controllore di accessi, e una volta che sai scegliere l'umore giusto, provare che l'input è genuino e tradurre i byte di nuovo in significato, Base64 smette di essere una fonte di bug misteriosi e diventa una routine che puoi automatizzare.
Una nota rapida sulle dimensioni: la decodifica rimpicciolisce i dati di circa un quarto (tre byte in uscita per ogni quattro caratteri in entrata), quindi l'output occupa sempre meno memoria dell'input. Non dovrai mai preoccuparti di una decodifica che esplode. Ora facciamo la conoscenza dello strumento.
La funzione che fa il lavoro
Ecco la firma completa, esattamente così come la riporta il PHP moderno:
base64_decode(string $string, bool $strict = false): string|false
In quella riga, tre parole fanno tutto il lavoro. $string non ha limiti di dimensione: un megabyte si decodifica in molto meno di un millisecondo, quindi nulla ti impedisce di decodificare un intero file in una sola chiamata. Il tipo di restituzione dichiara l'intero contratto: oppure una stringa di byte decodificati, oppure false. Nessuna eccezione, nessun codice di errore, nessun secondo canale. false è l'unico segnale che ricevi, quindi controllarla fa parte del lavoro. E una frase del manuale merita di essere memorizzata: i dati restituiti possono essere binari. Nel momento in cui il risultato contiene un PNG, un ZIP o un hash, non è una "stringa di testo" in alcun senso allentato, e PHP ti lascerà comunque trattarlo come tale, senza battere ciglio. Questa flessibilità è un superpotere e una trappola, e le sezioni sotto la tengono a bada.
Un rapido giro tra i tag di versione, perché il codice ereditato ha l'abitudine di dare le cose per scontate. La funzione è nel core dal PHP 4. Il suo parametro $strict è arrivato nel PHP 5.2.0, a novembre del 2006. Dal PHP 8.0, la firma porta tipi nativi veri e propri (il string e il bool che vedi sopra, più il tipo di restituzione string|false), quindi IDE e analizzatori statici sanno finalmente che la funzione può fallire. Dal PHP 8.1, passare null scatena un avviso di deprecazione; se intendi "nulla", scrivi esplicitamente '':
$decoded = base64_decode('');
var_dump($decoded); // string(0) ""
Modalità rigorosa o pulizia silenziosa
L'opzione $strict è un interruttore tra due personalità molto diverse. Spenta (il predefinito), il decoder è un dimenticone simpatico: ogni carattere fuori dall'alfabeto Base64 viene scartato in silenzio, il resto viene decodificato e nessuno viene avvisato. Il manuale lo dice chiaro: altrimenti, i caratteri non validi vengono scartati in silenzio. Accesa, il decoder è un controllore di accessi: il primo carattere che non riconosce vale all'intero carico utile un false.
Ecco il rapporto dei danni. Ogni riga qui sotto è il comportamento reale di base64_decode() su PHP 8.x:
| Input | Tollerante (predefinito) | Rigoroso |
|---|---|---|
Zm9vYmFy, pulito |
"foobar" |
"foobar" |
Zm9v\r\nYmFy, CRLF a metà stringa |
"foobar" |
"foobar" |
" Zm9vYmFy ", spazi ai due capi |
"foobar" |
"foobar" |
Zm9v\x0bYmFy, tabulazione verticale |
"foobar" |
false |
Zm9v\x00YmFy, byte NUL incorporato |
"foobar" |
false |
V@hpcy, @ fuori posto |
3 byte di scarto | false |
Zm9vY, cinque caratteri |
"foo", ultimo carattere scartato |
false |
Z, una singola lettera |
"", una stringa vuota |
false |
=Zm9, riempimento in testa |
"fo" |
false |
Zm9vYmFy==, riempimenti dopo un gruppo completo |
"foobar" |
false |
Zm9vYmFy==A, dati dopo i riempimenti |
"foobar" |
false |
Zm9vYmF, sette caratteri, nessun riempimento |
"fooba" |
"fooba" |
Tre righe meritano un secondo sguardo. La riga V@hpcy mostra perché la modalità tollerante è pericolosa ovunque l'input non sia fidato: il @ fuori posto non ferma la decodifica; semplicemente svanisce, e i tre byte che escono non significano nulla. La riga con la singola Z mostra che un risultato vuoto dimostra quasi nulla: un carico utile di un carattere solo "si decodifica" in una stringa vuota senza fallire. La riga Zm9vYmFy==A mostra il decoder che ignora volentieri i dati che appaiono dopo il riempimento: è così che un carico utile troncato o manomesso può sembrare perfettamente a posto.
Cosa lascia passare ancora la modalità rigorosa? Esattamente quattro caratteri di spazio: spazio, tabulazione, ritorno a capo e avanzamento di riga, in qualsiasi posizione, anche subito accanto ai segni =. È una scelta deliberata. I carichi utili di posta spezzati in stile MIME portano a capo CRLF dentro il flusso codificato, e la modalità rigorosa li smaltisce senza pre-elaborazione (la sezione sulla posta qui sotto spiega perché). Tutto il resto che non è un carattere dell'alfabeto, dai byte NUL alle tabulazioni verticali, si becca un false.
C'è una vera tolleranza che vale la pena conoscere, anche se non è una peculiarità di PHP: PHP integra il riempimento mancante per te, in silenzio. Il carico utile di sette caratteri Zm9vYmF (nessun riempimento) si decodifica in "fooba" esattamente come il suo cugino riempito Zm9vYmF=, in entrambi gli umori. RFC 4648 chiede il riempimento nel caso generale, quindi accettare una coda senza riempimento è un allentamento deliberato, e non è specifico di PHP: il RawStdEncoding di Go e il decoder di Java accettano lo stesso input senza riempimento. Se il tuo lato PHP e un sistema partner non sono d'accordo su un carico utile da caso limite, è di solito su un riempimento mancante che devi guardare.
Lo standard è d'accordo con l'umore rigoroso. RFC 4648, sezione 3.3, dice che le implementazioni devono rifiutare i dati codificati che contengono caratteri fuori dall'alfabeto, a meno che la specifica circostante non dica altrimenti (MIME è il classico caso che "dice altrimenti"). La stessa sezione spiega il perché: i caratteri fuori alfabeto possono essere sfruttati come canale nascosto, nascondendo informazioni nei caratteri che il tuo decoder scarta, e sono stati usati per scatenare bug dei decoder. Se il tuo input viene dal mondo esterno, la modalità rigorosa non è una questione di stile. È ciò che lo standard chiede.
Dimostrare che un carico utile è Base64
Un decoder che può fallire in silenzio merita una pipeline di validazione davanti a sé. Tre livelli, ciascuno dei quali cattura ciò che gli altri perdono.
Il livello uno è un controllo della forma con un'espressione regolare: solo caratteri dell'alfabeto, e al massimo due segni di riempimento alla fine.
$shapeLooksPlausible = preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $payload) === 1;
L'espressione regolare cattura lo scarto evidente (spazi vaganti, segni @, un riempimento a metà stringa) prima che qualsiasi altra cosa venga eseguita. Però non è un validatore: non vede che Zm9vYmFy= fa nove caratteri con un riempimento, che anche la modalità rigorosa rifiuta. Ed è esattamente per questo che esiste il livello due. La decodifica rigorosa è l'unico controllo che capisce la semantica del Base64, quindi ha l'ultima parola.
Il livello tre è quello che tutti dimenticano: gestire false esplicitamente, perché è l'unico segnale che ricevi.
function decode_payload(string $payload): string
{
$clean = str_replace(["\r", "\n"], '', $payload);
$decoded = base64_decode($clean, true);
if ($decoded === false) {
throw new InvalidArgumentException('Not a valid Base64 payload.');
}
return $decoded;
}
Lo str_replace() all'inizio è una comodità opzionale: la modalità rigorosa tollera già il CRLF, ma rimuoverlo tiene puliti i calcoli sulla lunghezza che farai dopo, perché il numero di caratteri di un carico utile pulito è sempre un multiplo di quattro. (Uno in più rispetto a un multiplo di quattro, come cinque o nove, è impossibile in Base64, e la modalità rigorosa lo rifiuterà.) Tieni presente che la funzione non lancia mai eccezioni da sola: il controllo tocca a te scriverlo.
Base64 sicuro per URL
Nella vita reale incontrerai un secondo alfabeto, ed è proprio quello che morde. Il Base64 standard usa + e /, due caratteri che sono guai negli URL: un + in una stringa di query viene interpretato come spazio prima ancora che PHP lo veda, e / è un separatore di percorso. RFC 4648, sezione 5, definisce la soluzione: l'alfabeto sicuro per URL e nomi di file, in cui + diventa -, / diventa _ e il riempimento = finale viene di solito eliminato per risparmiare caratteri. Il RFC è perentorio nel dire che questo "non debba essere considerato la stessa cosa della codifica base64", e il nome che sentirai più spesso è base64url. I JSON Web Token, i parametri state di OAuth, gli ID di sessione delle API e gli URL dei siti di video vivono tutti in questo dialetto.
Il lato del decoder è fatto di due passaggi: rimetti a posto l'alfabeto, poi ripristina il riempimento mancante. Ecco la funzione ausiliaria che finirai per riusare dappertutto:
function base64url_decode(string $data): string|false
{
$standard = strtr($data, '-_', '+/');
$missing = strlen($standard) % 4;
if ($missing !== 0) {
$standard .= str_repeat('=', 4 - $missing);
}
return base64_decode($standard, true);
}
var_dump(base64url_decode('aGk_PnRoZXJl')); // string(9) "hi?>there"
Il PHP moderno è dalla tua parte qui: integra il riempimento mancante per te, quindi il ripristino esplicito è doppia sicurezza (e mantiene il codice portabile a versioni PHP più vecchie). La direzione del pericolo è a senso unico. Se passi del testo URL-safe al decoder standard in modalità tollerante, i caratteri - e _ semplicemente non fanno parte dell'alfabeto standard, quindi vengono scartati. Il tuo output viene fuori più corto di quanto dovrebbe, senza errori, senza avvisi, niente. Esegui sempre prima lo scambio con strtr(), o meglio ancora, passa sempre per la funzione ausiliaria.
Una premessa onesta: se un carico utile URL-safe non contiene per caso né - né _, allora i due alfabeti sono identici byte per byte per quel dato in particolare, e non fa differenza quale decoder tu abbia usato. Il pericolo appare solo quando quei caratteri sono presenti, perché è l'unico punto in cui gli alfabeti divergono.
Testo, byte e set di caratteri
Base64 non ha idea di cosa significhino i tuoi byte, e il decoder di PHP eredita quella cecità. Il codec è cieco ai set di caratteri: restituisce gli stessi valori a 8 bit che gli sono entrati, che siano testo UTF-8, testo Windows-1252, un JPEG o un hash. Lo stesso PHP è sulla stessa lunghezza d'onda: una stringa è una sequenza di byte, nient'altro. Nel momento in cui vuoi mostrare il risultato o confrontarlo con altro testo, qualcuno deve rispondere a due domande: questo è testo in primo luogo, e se sì, in quale set di caratteri?
Il test pratico ha due caselle. Il binario quasi sempre si annuncia con byte NUL e caratteri di controllo bassi, e il testo che non è UTF-8 valido è la seconda casella. L'estensione mbstring (non abilitata per impostazione predefinita) ti dà il controllo rigoroso UTF-8:
function looks_binary(string $bytes): bool
{
if ($bytes === '') {
return false;
}
if (strpbrk($bytes, "\x00\x01\x02\x03\x04") !== false) {
return true;
}
return !mb_check_encoding($bytes, 'UTF-8');
}
var_dump(looks_binary("\x89PNG\r\n\x1a\n...png body")); // bool(true)
var_dump(looks_binary("héllo wörld, 日本語")); // bool(false)
Se il carico utile è testo in un set di caratteri legacy, convertilo prima che tocchi il tuo HTML. Windows-1252 è la codifica legacy più comune per i dati web e desktop, e la differenza tra essa e il semplice ISO-8859-1 decide se il byte 0x93 è un virgolettino curvo o un carattere di controllo invisibile:
// "café" in Windows-1252: l'è è un singolo byte, 0xE9
$legacy = base64_decode('Y2Fm6Q==', true);
$utf8 = mb_convert_encoding($legacy, 'UTF-8', 'Windows-1252');
var_dump($utf8); // string(5) "café": adesso l'è è due byte UTF-8
Un avvertimento sulla celebre mb_detect_encoding(): il manuale PHP stesso dice che il rilevamento automatico "non può mai essere del tutto affidabile" e lo paragona al decifrare un messaggio senza la chiave. Dagli un "café" in Windows-1252 e potrebbe risponderle Windows-1252; dagli un'intestazione PNG e può tranquillamente dire di nuovo Windows-1252, perché la famiglia di set di caratteri ISO-8859 è definita per ogni possibile valore di byte e quindi può combaciare con qualsiasi cosa. Tratta il rilevamento come l'ultima spiaggia, fidati di un set di caratteri dichiarato (un'intestazione, una riga di configurazione, l'ordinamento del database) quando ne esiste uno, e considera il resto UTF-8 o binario.
Quando il carico utile è un file
L'operazione file più comune è l'inverso di ciò che ha fatto qualche routine di export: arriva un file di testo .b64 e devi far tornare il file originale. Con la decodifica rigorosa e un controllo su false, questo è già in forma da produzione:
$encoded = file_get_contents('/var/www/uploads/blob.b64');
$decoded = base64_decode($encoded, true);
if ($decoded === false) {
http_response_code(400);
exit('That upload is not valid Base64.');
}
Le stringhe PHP sono solo byte, quindi nulla in questo percorso si preoccupa di sapere se il carico utile è un file di testo, un archivio ZIP o un video. Il calcolo delle dimensioni lavora a tuo favore: l'output decodificato è lungo tre quarti rispetto all'input codificato, quindi la decodifica non peggiora mai la memoria.
Una buona abitudine è lasciare che i byte si annuncino da soli prima di fidarti di qualsiasi etichetta. La classe finfo (l'estensione fileinfo, inclusa nelle build PHP standard) ti dice cosa sono in realtà i dati:
$mime = (new finfo(FILEINFO_MIME_TYPE))->buffer($decoded);
var_dump($mime); // string(9) "image/png"
$extensions = ['image/png' => 'png', 'application/pdf' => 'pdf', 'application/zip' => 'zip'];
$ext = $extensions[$mime] ?? 'bin';
$target = '/var/www/uploads/file-' . bin2hex(random_bytes(4)) . '.' . $ext;
file_put_contents($target, $decoded);
Quel passo finale conta più di quanto sembri. Un carico utile che dichiara di essere un'immagine ma si decodifica in qualcos'altro è esattamente il tipo di cosa che una seconda opinione smaschera. E se in seguito servi di nuovo il file ripristinato a un browser, il Content-Type che invii dovrebbe venire dallo stesso controllo finfo, non dal nome del file.
Data URI, il formato degli appunti
Un arrivo preferito: qualcuno incolla un'immagine in un modulo, e il front end ti passa un data URI completo: data:image/png;base64,iVBORw0KGgo.... RFC 2397 definisce la forma: data:, un tipo media opzionale, un'opzione ;base64 opzionale, una virgola, e poi i dati. Se l'opzione è presente, il carico utile è Base64; se è assente, il carico utile è testo semplice percent-encodato, più raro ma legittimo. Se il tipo media è omesso, il predefinito è text/plain;charset=US-ASCII. Perché usare Base64 qui? Perché un URI non può contenere al sicuro byte grezzi o virgole, e Base64 ti dà un unico alfabeto che non richiede caratteri di fuga.
function split_data_uri(string $uri): ?array
{
if (!str_starts_with($uri, 'data:') || !str_contains($uri, ',')) {
return null;
}
$meta = substr($uri, 5, strpos($uri, ',') - 5);
$payload = substr($uri, strpos($uri, ',') + 1);
$isBase64 = str_ends_with($meta, ';base64');
$mime = $isBase64 ? substr($meta, 0, -7) : $meta;
if ($mime === '') {
$mime = 'text/plain;charset=US-ASCII';
}
return [$mime, $isBase64, $payload];
}
$uri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ';
[$mime, $isBase64, $payload] = split_data_uri($uri);
var_dump($mime); // string(9) "image/png"
Due trappole vivono in questo formato. La prima è l'opzione ;base64 mancante: un data URI legittimo senza l'opzione porta un carico utile percent-encodato, e passarlo a base64_decode() produce dati spazzatura. La seconda è il tipo media dichiarato: è un indizio del mittente, non un fatto. Il controllo finfo della sezione file è il tuo fatto. E ricorda il consiglio dello stesso RFC, per cui i data URI sono utili solo per valori brevi; un'immagine di qualche megabyte dentro un URL è un cattivo odore, non un modello.
JWT: token in cui puoi dare un'occhiata
Il carico utile Base64 più famoso del web è il JSON Web Token, e il meno spaventoso una volta che ne conosci la forma. Secondo RFC 7519, un JWT compatto è tre parti Base64 URL-safe separate da punti: intestazione, carico utile e firma, ciascuna codificata senza riempimento e senza a capo (RFC 7515 è esplicito nel dire che nessun carattere extra possa infilarcisi). L'intestazione e il carico utile sono JSON semplice, ed è per questo che tutti possono leggerli, e per questo che tutti dovrebbero capire il prossimo paragrafo prima di toccare un token.
Leggere le prime due parti è un lavoro di cinque righe con la funzione ausiliaria di sopra, ed è un ottimo modo per disarmare il mistero di un token:
$token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.8GljXWCrvkTYln_WtTVhyWSzflOC1iGL8jBDUHQmaEE';
[$headerPart, $payloadPart] = explode('.', $token);
$header = json_decode(base64url_decode($headerPart), true);
$payload = json_decode(base64url_decode($payloadPart), true);
var_dump($header);
// array(2) { ["alg"] => string(5) "HS256" ["typ"] => string(3) "JWT" }
var_dump($payload);
// array(3) { ["sub"] => string(10) "1234567890" ["name"] => string(8) "John Doe" ["iat"] => int(1516239022) }
Adesso la parte che conta: la terza parte è una firma, e le due parti che hai appena decodificato non sono segrete né autenticate. Chiunque abbia una cattura di pacchetti può leggerle, e chiunque abbia un editor di testo può riscriverle. Fidarsi del carico utile prima di verificare la firma è il classico bug dei JWT. In produzione, non costruire tu quel controllo. La risposta della comunità è il pacchetto firebase/php-jwt, attualmente alla v7, conforme a RFC 7519 e che richiede PHP 8.0 o più recente. Installalo con Composer:
composer require firebase/php-jwt
Poi l'API verifica per prima e ti passa il carico utile solo se la firma torna:
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$secret = 'correct-horse-battery-staple-long-enough-secret';
try {
$claims = JWT::decode($token, new Key($secret, 'HS256'));
var_dump($claims->sub); // una proprietà, e solo dopo che la firma è tornata buona
} catch (UnexpectedValueException $e) {
// token non valido, firma errata o claim scaduti
}
Una nota sulle versioni: la v7 della libreria impone una lunghezza minima delle chiavi per gli algoritmi HMAC, quindi un segreto HS256 più corto di 32 byte viene rifiutato con una DomainException prima ancora di verificare la firma. Tieni i tuoi segreti lunghi; la libreria non ti lascia dimenticare.
Presta attenzione all'ordine in quell'API: JWT::decode() lancia un'eccezione su firma errata, token scaduto o algoritmo mancante invece di restituirti dati spazzatura, quindi un carico utile che ricevi indietro è uno su cui puoi fidarti. La versione fatta a mano di sopra serve a capire, e a dare un'occhiata a token che non erano destinati a te; la libreria serve a fidarsi.
HTTP Basic Auth, l'intestazione più antica
L'intestazione di autenticazione più antica del web viaggia ancora sul Base64. Secondo RFC 7617, una richiesta HTTP Basic invia Authorization: Basic seguito dalla codifica Base64 di username:password. Il RFC è esplicito nel dire che questa è codifica, non protezione: chiunque abbia una cattura di pacchetti può decodificare entrambe le metà con un tasto solo. Il tuo lavoro sul lato del decoder è analizzare l'intestazione, decodificare in modo rigoroso e confrontare con una funzione a tempo sicuro.
function basic_credentials(string $header): ?array
{
if (!str_starts_with($header, 'Basic ')) {
return null;
}
$decoded = base64_decode(substr($header, 6), true);
if ($decoded === false || !str_contains($decoded, ':')) {
return null;
}
[$user, $password] = explode(':', $decoded, 2);
return [$user, $password];
}
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$creds = basic_credentials($header);
if ($creds !== null
&& hash_equals('alice', $creds[0])
&& hash_equals('secret123', $creds[1])
) {
// autenticato
}
Due dettagli tengono al sicuro questa operazione. Il limite 2 di explode() conta perché le password possono legittimamente contenere due punti, e il confronto dovrebbe essere hash_equals(), mai ==, così un attaccante non può passare a tempo attraverso la tua lista utenti. E servilo solo su HTTPS; su una connessione in chiaro, il livello Base64 è pura apparenza.
Email, dove tutto ha avuto inizio
Base64 è nato per un problema specifico: il trasporto della posta portava solo ASCII a 7 bit, eppure la gente voleva inviare file binari. Lo standard MIME (RFC 2045, sezione 6.8) ha reso Base64 una delle codifiche di trasferimento binario e ha aggiunto due regole di casa. Prima: le righe codificate non devono superare i 76 caratteri. Seconda: il software di decodifica deve ignorare ogni carattere fuori dall'alfabeto, a capo compresi. Questa seconda regola è esattamente il motivo per cui il decoder di PHP, in entrambi gli umori, smaltisce un carico utile spezzato con CRLF senza alcuna pre-elaborazione da parte tua. (È anche l'origine della tolleranza verso \r\n che hai visto nella tabella della modalità rigorosa qui sopra.)
$png = "\x89PNG\r\n\x1a\n" . random_bytes(256);
$wrapped = chunk_split(base64_encode($png), 76, "\r\n");
// più tardi, sulla ricezione, senza nessuna pulizia:
$decoded = base64_decode($wrapped, true);
var_dump($decoded === $png); // bool(true): ogni byte ha fatto il viaggio di andata e ritorno
Due note pratiche. Prima: la spezzatura aggiunge peso: con un CRLF ogni 76 caratteri, un allegato di 100 KB arriva come circa 137 KB di testo (il solito fattore quattro terzi, più il sovrapprezzo degli a capo). Seconda: per la posta reale con intestazioni, più parti e parenti quoted-printable, l'estensione opzionale mailparse disseziona i messaggi RFC 822 completi parte per parte; per un singolo allegato noto, la decodifica rigorosa è tutto ciò che ti serve.
Armatura PEM: chiavi e certificati
Certificati e chiavi viaggiano in armatura PEM: un'etichetta BEGIN, un blocco di Base64 in righe da 64 caratteri, e un'etichetta END. La lunghezza di riga di 64 caratteri è una convenzione ereditata dalla specifica originale Privacy Enhanced Mail (RFC 1421), e gli strumenti OpenSSL se l'aspettano, quindi conta quando ri-armi. Quando decodifichi, non conta per nulla: il decoder ignora semplicemente gli a capo.
$pem = file_get_contents('/etc/ssl/my-key.pem');
preg_match('/-----BEGIN ([A-Z ]+)-----\s*(.*?)\s*-----END \1-----/s', $pem, $m);
$label = $m[1];
$der = base64_decode(preg_replace('/\s+/', '', $m[2]), true);
if ($der === false) {
// alla fine non è Base64
}
var_dump($label); // string(11) "PRIVATE KEY"
I byte decodificati sono DER, una serializzazione binaria compatta, ed è con quella che le funzioni openssl_* lavorano in ultima analisi. Il riferimento inverso \1 nell'espressione regolare è l'eroe silenzioso: garantisce che l'etichetta END combaci con l'etichetta BEGIN, ed è così che eviti di cucire l'END di un certificato sul BEGIN di una chiave quando un file contiene più blocchi.
Flussi e carichi utili grandi
La decodifica è la direzione che ti aiuta: l'output è tre quarti delle dimensioni dell'input, quindi la pressione di memoria da Base64 è rara. Resta che quando un file .b64 di centinaia di megabyte atterra su disco, hai due strumenti per tenere piatta l'impronta.
Il primo è la decodifica a blocchi. Spezza l'input ripulito in pezzi la cui lunghezza è un multiplo di quattro caratteri, decodifica ciascun pezzo in modo rigoroso e concatena. Ogni blocco è un carico utile valido e autosufficiente, quindi nulla si perde ai bordi, e un file corrotto fallisce in fretta con un offset che puoi segnalare.
$clean = str_replace(["\r", "\n"], '', file_get_contents('/var/www/uploads/huge.b64'));
$decoded = '';
$chunkSize = 4 * 50000; // un multiplo di quattro caratteri, circa 150 KB di output per chiamata
for ($offset = 0; $offset < strlen($clean); $offset += $chunkSize) {
$part = base64_decode(substr($clean, $offset, $chunkSize), true);
if ($part === false) {
exit('Corrupted payload near offset ' . $offset);
}
$decoded .= $part;
}
Un megabyte di Base64 si decodifica in molto meno di un millisecondo su hardware moderno, quindi questo loop costa quasi nulla; sceglilo per le sue proprietà di validazione e segnalazione, non per la velocità.
Il secondo strumento è un cittadino del mondo dello streaming: il filtro di flusso convert.base64-decode. Funziona su qualsiasi flusso PHP, quindi puoi decodificare direttamente da un puntatore di file, da php://input o da un flusso in memoria senza tenere mai l'intero testo codificato in una sola variabile. Come la funzione tollerante, salta semplicemente ogni carattere fuori dall'alfabeto Base64:
$in = fopen('/var/www/uploads/huge.b64', 'rb');
$out = fopen('/var/www/uploads/huge.bin', 'wb');
stream_filter_append($in, 'convert.base64-decode', STREAM_FILTER_READ);
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);
Quale strumento scegli? Il filtro quando i dati fluiscono attraverso un flusso e vuoi che PHP si occupi dell'idraulica; il loop a blocchi quando ti serve la validazione per blocco, la segnalazione di avanzamento o l'offset della corruzione.
Database, file di configurazione e variabili d'ambiente
Base64 è un contenitore di testo, ed è per questo che compare in posti in cui non te l'aspetti. Nei database, un blob binario (un file, un'icona, una struttura serializzata) può vivere in una colonna TEXT come Base64, sopravvivendo a ogni strumento che dà per scontato il testo. Aspettati che il valore memorizzato sia circa il 33 percento più grande dell'originale, e dimensiona le colonne di conseguenza. Nei file di configurazione e nelle variabili d'ambiente, Base64 è il trucco per far passare di contrabbando valori che altrimenti romperebbero il formato: un DSN del database con punti e virgola, una password con virgolette, un valore con un a capo.
// .env o configurazione, scritto da chi si occupa di operations:
// DB_DSN_B64 = cGc6aG9zdD1kYjtwYXNzd29yZD1xdSJvdGU=
$dsn = base64_decode(getenv('DB_DSN_B64') ?: '', true);
if ($dsn === false) {
exit('DB_DSN_B64 is not valid Base64.');
}
// $dsn è adesso: pg:host=db;password=qu"ote
La stessa cautela vale qui due volte. Prima: questa è sicurezza di formato, non segretezza: nel momento in cui uno sviluppatore legge il file di configurazione, può decodificare il valore in una sola chiamata. Non memorizzare mai un segreto come Base64 e chiamarlo cifrato. Seconda: valida all'avvio: un valore di variabile corrotto o incollato a metà è un false dalla chiamata rigorosa, e un controllo di una riga trasforma un errore di runtime criptico in un messaggio di avvio azionabile.
Dalla riga di comando
Non tutta la decodifica avviene all'interno di una richiesta web. Gli script CLI, i job cron e i comandi in una riga decodificano Base64 in continuazione, e la riga di comando è il punto in cui la funzione incontra php://stdin:
php -r 'fwrite(STDOUT, base64_decode(file_get_contents("php://stdin"), true));' < payload.b64 > restored.bin
La shell ha già la sua utility Base64 (coreutils base64 -d), ed è sufficiente per il lavoro veloce; il comando PHP in una riga è per quando il passo successivo è logica PHP: scrivere in un database, chiamare un'API, eseguire una validazione. Due insidie specifiche della shell. L'output di una decodifica è un flusso di byte grezzi, quindi mandalo a un file o a un comando che capisce i byte, non a un terminale che li rovinerebbe. E lascia l'opzione rigorosa attiva anche nel comando in una riga, perché un incollaggio troncato dentro un terminale merita un false, non tre byte di scarto.
Trappole con accento PHP
Un rapido giro delle trappole specifiche di PHP, raccolte in un unico posto:
- Il predefinito tollerante è la più grossa.
base64_decode('V@hpcy')restituisce 3 byte di scarto senza alcun avviso, quindi ogni decoder di input non fidato ha bisogno dell'opzione rigorosa e di un controllo sufalse. - Un singolo carattere si decodifica in una stringa vuota in modalità tollerante, e lo fa anche una stringa fatta solo di spazi. Un risultato vuoto dimostra quasi nulla; solo
falsesignifica fallimento, e lo ottieni solo in modalità rigorosa. - Il
+in una stringa di query è già uno spazio prima che PHP lo veda. Se un client invia?token=abc+defsenza percent-encoding, PHP ti passaabc def(è il comportamento della codifica dei moduli, condiviso daparse_str()eurldecode()), e nessuna magia di decodifica riporta il più. Il Base64 URL-safe (senza nessun più) è la soluzione per i token negli URL. - Il riempimento mancante viene integrato per te, in silenzio. Sette caratteri si decodificano come otto; comoda cosa, ma significa che un carico utile troncato di un paio di segni di riempimento può comunque decodificarsi senza lamentazioni, quindi una decodifica pulita non dimostra mai del tutto che il carico utile è arrivato intero (gli encoder raw di Go e Java sono altrettanto tolleranti).
- Il fantasma di
mbstring.func_overload. L'impostazione da tempo deprecata che riscrivevastrlen()e compagni per contare i caratteri (rimossa nel PHP 8.0) rompeva i calcoli dei byte del Base64 sulle stringhe UTF-8. Il codice legacy che erediti può ancora portare commenti e workaround per essa. Eliminali. - I byte decodificati non sono una stringa UTF-8. Eseguire
preg_match()con l'opzione/uomb_substr()su binario decodificato è una fonte istantanea di errori di "input non valido". Annusa prima, poi decidi. - Passare
nullè deprecato dal PHP 8.1. Se una variabile può essere null, coalescala a''prima della chiamata. $_GETe compagni vengono decodificati con le regole dei moduli, non con le regole degli URL. Se un valore è arrivato percent-encodato,rawurldecode()è l'inverso più sicuro, perché lascia+dove sta.
Una breve storia di base64_decode
Base64 in sé è più vecchio di gran parte del web moderno (lo standard che lo governa, RFC 4648, risale al 2006, e codificò la codifica MIME del 1996, che a sua volta discende dall'armatura PEM dei primi anni Novanta). La storia di PHP è un piccolo registro delle modifiche tutto suo.
PHP 4 spedì base64_decode() come funzione di core senza opzioni e senza modalità rigorosa; l'umore tollerante era l'unico umore, e non c'era modo di chiedere al decoder di lamentarsi. PHP 5.2.0, a novembre del 2006, aggiunse l'opzione $strict, e la voce del registro delle modifiche vale la lettura: fu aggiunta per far rispettare la conformità a RFC 3548, la predecessora dell'odierna RFC 4648. Quell'unica opzione si rivelò l'aggiunta più utile di tutta la vita della funzione.
Poi vennero gli anni del debug. PHP 5.3 corresse una serie di bug della modalità rigorosa in due release puntuali: bug #52327 (riempimento iniziale gestito male in modalità rigorosa, corretto in 5.3.4) e bug #55273 (spazi dopo il riempimento rifiutati in modalità rigorosa, corretto in 5.3.9). (Anche una correzione di overflow di intero del 2016 è archiviata sotto il nome di questa funzione: bug #72836, ufficialmente intitolato "overflow di intero in base64_decode che causa corruzione dell'heap" e corretto in 5.6.25, ma il codice di riproduzione del bug report stesso e la funzione corretta mostrano che l'overflow vero era nel calcolo della lunghezza di base64_encode(), non nel decoder; il titolo è un errore di denominazione ereditato dal report originale.) Ogni correzione rese più rigoroso il comportamento che vedi nella tabella qui sopra. PHP 8.0 diede a entrambe le funzioni Base64 tipi nativi per parametri e restituzioni, la firma che hai visto in cima a questo articolo, e la stessa linea di release rimosse mbstring.func_overload, l'impostazione che per anni aveva rotto in silenzio i calcoli sui byte. PHP 8.1 deprecò il passaggio di null a entrambe. Da allora, la superficie è congelata: un parametro, un'opzione, un tipo di restituzione, immutati.
Qualche gioia da nerd
Dato che si tratta di un riferimento lungo, ecco alcuni fatti specifici di PHP che sono semplicemente divertenti:
- L'identità vuota.
base64_encode('')ebase64_decode('')sono entrambi''. Le funzioni trattano il vuoto come un valore di prima classe in entrambe le direzioni, senza coinvolgerefalse. - Un indirizzo bizzarro. Nel manuale di PHP, entrambe le funzioni Base64 vivono nel capitolo "URLs" del libro "Altre Estensioni di Base". Non esiste un capitolo dedicato alla "codifica"; è lì che le troverai, in cima alla lista del capitolo, davanti a
parse_url()e ai suoi amici. - Il decoder è un omomorfismo. Un classico commento utente di php.net osserva che la funzione è un omomorfismo tra stringhe segmentate a modulo 4 e stringhe segmentate a modulo 3, che è il modo formale di dire che ogni spezzatura a multipli di quattro è una spezzatura valida. È per questo che la sezione sulla decodifica a blocchi funziona in primo luogo, e per questo che un file da 1 MB può essere decodificato a fette da 50 KB con perdita zero.
- Un parametro, un'opzione. In più di vent'anni,
base64_decode()ha guadagnato esattamente un parametro ($strict), ebase64_encode()non ne ha guadagnato nessuno. - Ha fratelli più grandi. La stessa estensione di core porta anche
convert_uuencode()econvert_uudecode()(elencate nel manuale sotto "Funzioni sulle stringhe"), i relitti dell'era del dial-up, quando l'uuencode era il trasporto di scelta per i binari. Non ti serviranno quasi mai, ma se un antico file.uudovesse mai finire nella tua casella di posta, PHP può aprirlo. - La modalità rigorosa lascia una porta aperta per la posta. I quattro caratteri di spazio (spazio, tabulazione, ritorno a capo e avanzamento di riga) passano attraverso la modalità rigorosa deliberatamente, quindi un allegato spezzato in stile MIME non ha bisogno di pre-elaborazione. Tutto il resto, byte NUL inclusi, è un
false.
L'altra direzione
Questa era il lato del decoder, ed è dove vive gran parte del dolore, perché la decodifica è dove incontri i dati degli altri: le loro scelte di riempimento, i loro a capo, i loro set di caratteri, i loro token. L'altra direzione, quella che trasforma i byte in una stringa Base64 con base64_encode(), è un animale più tranquillo: non fallisce mai, non ha una modalità rigorosa, e il suo set di trappole (doppia codifica, spezzature non in sintonia, il conto delle dimensioni) ha la sua guida. La codifica Base64 in PHP, collegata da questa pagina, copre l'encoder con la stessa profondità.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Codifica Base64 in PHP: una guida completa