Decodificação Base64 em PHP: um guia completo
Ela aparece num ticket de suporte, num log de API, num arquivo de configuração ou no meio de uma URL: uma longa sequência de letras, dígitos, de vez em quando um + ou um /, e talvez um = ou dois no final. Você a reconhece num piscar de olhos. Base64 é um formato de binário para texto: ele reescreve cada três bytes de dados brutos como quatro caracteres tirados de um alfabeto de 64 letras, e um par de sinais = fecha a cauda quando a contagem de bytes não é um múltiplo de três. Decodificar é a direção de encolhimento dessa troca: quatro caracteres entram, três bytes saem. A página inicial deste site mostra o formato passo a passo, então este artigo gasta sua energia onde ela pertence: no lado PHP do trabalho.
Primeiro, a notícia principal. O PHP entrega um decodificador Base64 no núcleo desde o PHP 4. O base64_decode() não precisa de extensão, de pacote do Composer nem de configuração, e roda em todo lugar onde o PHP roda. A notícia menos boa: o humor padrão dela engole entradas corrompidas em silêncio e te entrega lixo sem dizer uma palavra. A notícia boa melhora: um flag ($strict) transforma a função num porteiro de verdade, e quando você souber escolher o humor, provar que a entrada é de verdade e traduzir os bytes de volta para significado, o Base64 para de ser uma fonte de bugs misteriosos e vira uma rotina que dá para automatizar.
Uma nota rápida sobre tamanho: decodificar encolhe os dados em mais ou menos um quarto (três bytes saem para cada quatro caracteres que entram), então a saída sempre ocupa menos memória do que a entrada. Você nunca precisa se preocupar com uma decodificação que estoure. Agora, vamos conhecer a ferramenta.
A função que faz o trabalho
Aqui está a assinatura completa, exatamente como o PHP moderno a reporta:
base64_decode(string $string, bool $strict = false): string|false
Três palavras naquela linha fazem todo o trabalho. O $string não tem limite de tamanho: um megabyte decodifica em bem menos de um milissegundo, então nada te impede de decodificar um arquivo inteiro numa única chamada. O tipo de retorno declara o contrato inteiro: ou uma string de bytes decodificados, ou false. Não há exceções, não há códigos de erro, não há segundo canal. O false é o único sinal que você recebe, então checar ele faz parte do trabalho. E uma frase do manual merece ser decorada: os dados retornados podem ser binários. No momento em que o resultado contém um PNG, um ZIP ou um hash, ele não é uma "string de texto" em nenhum sentido solto, e o PHP vai deixar você tratá-la como tal com toda a tranquilidade. Essa flexibilidade é um superpoder e uma armadilha, e as seções abaixo a mantêm sob controle.
Uma passagem rápida pelas versões, porque código herdado cria o hábito de dar as coisas como certas. A função está no núcleo desde o PHP 4. O parâmetro $strict chegou no PHP 5.2.0, em novembro de 2006. Desde o PHP 8.0, a assinatura carrega tipos nativos de verdade (o string e o bool que você vê acima, mais o retorno string|false), então IDEs e analisadores estáticos finalmente sabem que a função pode falhar. Desde o PHP 8.1, passar null gera um aviso de deprecação; se a intenção é "nada", escreva '' explicitamente:
$decoded = base64_decode('');
var_dump($decoded); // string(0) ""
Modo strict ou limpeza silenciosa
A flag $strict é um interruptor entre duas personalidades muito diferentes. Desligado (o padrão), o decodificador é um esquecido simpático: todo caractere fora do alfabeto Base64 é descartado em silêncio, o resto é decodificado e ninguém fica sabendo. O manual diz sem rodeios: caso contrário, caracteres inválidos serão descartados em silêncio. Ligado, o decodificador é um porteiro: o primeiro caractere que ele não reconhece ganha para o payload inteiro um false.
Aqui está o relatório de danos. Cada linha abaixo é comportamento real do base64_decode() no PHP 8.x:
| Entrada | Tolerante (padrão) | Strict |
|---|---|---|
Zm9vYmFy, limpo |
"foobar" |
"foobar" |
Zm9v\r\nYmFy, CRLF no meio da string |
"foobar" |
"foobar" |
" Zm9vYmFy ", espaços nas duas pontas |
"foobar" |
"foobar" |
Zm9v\x0bYmFy, tabulação vertical |
"foobar" |
false |
Zm9v\x00YmFy, byte NUL embutido |
"foobar" |
false |
V@hpcy, @ perdido |
3 bytes de lixo | false |
Zm9vY, cinco caracteres |
"foo", último caractere descartado |
false |
Z, uma única letra |
"", uma string vazia |
false |
=Zm9, padding na frente |
"fo" |
false |
Zm9vYmFy==, pads depois de um grupo completo |
"foobar" |
false |
Zm9vYmFy==A, dados depois dos pads |
"foobar" |
false |
Zm9vYmF, sete caracteres, sem pads |
"fooba" |
"fooba" |
Três linhas merecem um segundo olhar. A linha do V@hpcy mostra por que o modo tolerante é perigoso em qualquer lugar onde a entrada não é confiável: o @ perdido não para a decodificação; ele simplesmente desaparece, e os três bytes que saem não significam nada. A linha do Z sozinho mostra que um resultado vazio prova quase nada; um payload de um caractere "decodifica" para uma string vazia sem falhar. A linha do Zm9vYmFy==A mostra o decodificador ignorando tranquilamente dados que aparecem depois do padding, e é assim que um payload truncado ou adulterado pode parecer perfeitamente normal.
O que o modo strict ainda deixa passar? Exatamente quatro caracteres de espaço em branco: espaço, tabulação, carriage return e line feed, em qualquer posição, até colado nos sinais =. Isso é de propósito. Payloads de e-mail embrulhados em MIME carregam quebras de linha CRLF dentro do stream codificado, e o modo strict os devora sem pré-processamento (a seção de e-mail abaixo explica o porquê). Tudo o mais que não for caractere do alfabeto, de bytes NUL a tabulações verticais, ganha um false.
Há uma tolerância genuína que vale conhecer, ainda que não seja um tique do PHP: o PHP completa o padding que falta por você, em silêncio. O payload de sete caracteres Zm9vYmF (sem nenhum pad) decodifica para "fooba" exatamente como o parente com padding Zm9vYmF=, nos dois humores. O RFC 4648 pede padding no caso geral, então aceitar uma cauda sem padding é uma relaxação deliberada, e não é específica do PHP: o RawStdEncoding do Go e o decodificador do Java aceitam a mesma entrada sem padding. Se o seu lado PHP e um sistema parceiro discordam sobre um payload de caso-limite, um pad faltando é geralmente onde procurar.
O padrão concorda com o humor strict. O RFC 4648, seção 3.3, diz que as implementações devem rejeitar dados codificados que contenham caracteres fora do alfabeto, a menos que a especificação ao redor diga o contrário (o MIME é o caso clássico de "dizer o contrário"). A mesma seção explica o porquê: caracteres fora do alfabeto podem ser explorados como canal oculto, escondendo informação em caracteres que o seu decodificador joga fora, e eles já foram usados para disparar bugs em decodificadores. Se a sua entrada vem do mundo exterior, o modo strict não é uma questão de estilo. É o que o padrão pede.
Provando que um payload é Base64
Um decodificador que pode falhar em silêncio merece um pipeline de validação na frente dele. Três camadas, cada uma pegando o que as outras perdem.
A camada um é uma verificação de formato com uma expressão regular: apenas caracteres do alfabeto, e no máximo dois pads bem no final.
$shapeLooksPlausible = preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $payload) === 1;
A regex pega o lixo óbvio (espaços perdidos, sinais @, um pad no meio da string) antes de qualquer outra coisa rodar. Mas não é um validador: ela não enxerga que Zm9vYmFy= são nove caracteres com um pad, o que o modo strict também recusa. É exatamente por isso que a camada dois existe. A decodificação strict é a única verificação que entende a semântica do Base64, então ela tem a palavra final.
A camada três é a que todo mundo esquece: trate o false explicitamente, porque é o único sinal que você recebe.
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;
}
O str_replace() na frente é um conforto opcional: o modo strict já tolera CRLF, mas remover isso mantém qualquer cálculo de comprimento que você fizer depois limpo, porque a contagem de caracteres de um payload limpo é sempre um múltiplo de quatro. (Um a mais que um múltiplo de quatro, como cinco ou nove, é impossível em Base64, e o modo strict vai recusar.) Note que a função nunca lança sozinha; a verificação é sua de escrever.
Base64 seguro para URL
No mundo real você vai encontrar um segundo alfabeto, e é ele que morde. O Base64 padrão usa + e /, dois caracteres que causam problema em URLs: um + numa query string é interpretado como espaço antes do PHP sequer vê-lo, e / é um separador de caminho. O RFC 4648, seção 5, define a correção: o alfabeto seguro para URL e nome de arquivo, onde + vira -, / vira _, e o padding = no final costuma ser descartado para economizar caracteres. O RFC é taxativo: isso "não deve ser considerado o mesmo que a codificação base64", e o nome que você vai ouvir mais é base64url. JSON Web Tokens, parâmetros de state do OAuth, IDs de sessão de API e URLs de sites de vídeo todos moram nesse dialeto.
O lado decodificador tem dois passos: troque o alfabeto de volta e depois restaure qualquer padding que faltar. Aqui está o helper que você vai acabar reutilizando em todo lugar:
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"
O PHP moderno está a seu favor aqui: ele completa o padding que falta por você, então o restore explícito é redundância por garantia (e mantém o seu código portável para versões antigas do PHP). A direção do perigo é de mão única. Se você alimentar o decodificador padrão em modo tolerante com texto URL-safe, os caracteres - e _ simplesmente não estão no alfabeto padrão, então são descartados. A sua saída sai mais curta do que deveria, sem erro, sem aviso, nada. Rode sempre a troca do strtr() primeiro, ou melhor, passe sempre pelo helper.
Um aviso honesto: se um payload URL-safe por acaso não contiver nem - nem _, os dois alfabetos são idênticos byte a byte para aquele dado específico, e não importa qual decodificador você usou. O perigo aparece apenas quando esses caracteres estão presentes, porque é o único lugar onde os alfabetos diferem.
Texto, bytes e conjuntos de caracteres
O Base64 não tem a menor ideia do que os seus bytes significam, e o decodificador do PHP herda essa cegueira. O codec é cego a charset: ele devolve os mesmos valores de 8 bits que foram para dentro, seja texto UTF-8, texto Windows-1252, um JPEG ou um hash. O PHP em si está na mesma página: uma string é uma sequência de bytes, nada mais. No momento em que você quer exibir o resultado ou compará-lo com outro texto, alguém precisa responder a duas perguntas: isso é texto de algum jeito, e se sim, em qual charset?
O teste prático tem dois baldes. Binário quase sempre se anuncia com bytes NUL e bytes de controle baixos, e texto que não é UTF-8 válido é o segundo balde. A extensão mbstring (não habilitada por padrão) te dá a verificação UTF-8 strict:
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)
Quando o payload é texto em um charset legado, converta-o antes que ele toque no seu HTML. O Windows-1252 é a codificação legado mais comum para dados de web e desktop, e a diferença entre ele e o ISO-8859-1 puro decide se o byte 0x93 é uma aspa curva ou um caractere de controle invisível:
// "café" em Windows-1252: o é é um byte só, 0xE9
$legacy = base64_decode('Y2Fm6Q==', true);
$utf8 = mb_convert_encoding($legacy, 'UTF-8', 'Windows-1252');
var_dump($utf8); // string(5) "café": agora o é é dois bytes UTF-8
Um aviso sobre o famoso mb_detect_encoding(): o próprio manual do PHP diz que a detecção automática "nunca pode ser totalmente confiável", e compara isso com descriptografar uma mensagem sem a chave. Alimente com um "café" Windows-1252 e ele pode dizer Windows-1252; alimente com um header PNG e ele pode dizer Windows-1252 de boa, porque a família de charsets ISO-8859 é definida para todo valor de byte possível e, portanto, pode corresponder a qualquer coisa. Trate a detecção como último recurso, confie num charset declarado (um header, uma linha de configuração, uma colação de banco de dados) sempre que existir um, e deixe o resto padrão em UTF-8 ou binário.
Quando o payload é um arquivo
O trabalho com arquivo mais comum é o inverso do que alguma rotina de exportação fez: um arquivo de texto .b64 chega, e você precisa do arquivo original de volta. Com decodificação strict e um cheque de false, isso já tem forma de produção:
$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.');
}
Strings do PHP são só bytes, então nada neste caminho se importa se o payload é um arquivo de texto, um arquivo ZIP ou um vídeo. A conta de tamanho joga a seu favor: a saída decodificada tem três quartos do comprimento da entrada codificada, então decodificar nunca piora a memória.
Um bom hábito é deixar os bytes se anunciarem antes de você confiar em qualquer rótulo. A classe finfo (a extensão fileinfo, que vem nos builds padrão do PHP) te diz o que os dados realmente são:
$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);
Aquele último passo importa mais do que parece. Um payload que alega ser imagem mas decodifica para outra coisa é exatamente o tipo de coisa que uma segunda opinião pega. E se você servir o arquivo restaurado de volta a um navegador mais tarde, o Content-Type que você enviar deve vir da mesma verificação finfo, não do nome do arquivo.
Data URIs, o formato da área de transferência
Uma chegada favorita: alguém cola uma imagem num formulário, e o front end te entrega uma data URI completa: data:image/png;base64,iVBORw0KGgo.... O RFC 2397 define o formato: data:, um media type opcional, um flag ;base64 opcional, uma vírgula, e então os dados. Quando a flag está presente, o payload é Base64; quando está ausente, o payload é texto puro percent-encoded, mais raro mas legal. Se o media type for omitido, o padrão é text/plain;charset=US-ASCII. Por que Base64 aqui em primeiro lugar? Porque uma URI não pode conter com segurança bytes crus ou vírgulas, e o Base64 te dá um alfabeto que não precisa de escapamento.
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"
Duas armadilhas moram neste formato. A primeira é a flag ;base64 faltando: uma data URI legal sem a flag carrega um payload percent-encoded, e passar isso pelo base64_decode() produz lixo. A segunda é o media type alegado: ele é uma dica do remetente, não um fato. A verificação finfo da seção de arquivos é o seu fato. E lembre do conselho do próprio RFC de que data URIs só são úteis para valores curtos; uma imagem de vários megabytes dentro de uma URL é um mau cheiro, não um padrão.
JWTs: tokens em que você pode dar uma espiada
O payload Base64 mais famoso da web é o JSON Web Token, e o menos assustador quando você conhece o formato. Segundo o RFC 7519, um JWT compacto é três partes Base64 URL-safe separadas por pontos: um header, um payload e uma assinatura, cada uma codificada sem padding e sem quebras de linha (o RFC 7515 é explícito: nenhum caractere extra pode se infiltrar). O header e o payload são JSON puro, que é por que todo mundo consegue lê-los, e por que todo mundo deveria entender o próximo parágrafo antes de tocar num token.
Ler as duas primeiras partes é trabalho de cinco linhas com o helper de cima, e é uma ótima maneira de desmistificar um 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) }
Agora a parte que importa: a terceira parte é uma assinatura, e as duas partes que você acabou de decodificar não são secretas nem autenticadas. Qualquer um com uma captura de pacotes consegue lê-las, e qualquer um com um editor de texto consegue reescrevê-las. Confiar no payload antes de verificar a assinatura é o bug clássico de JWT. Para produção, não faça essa verificação na mão. A resposta da comunidade é o pacote firebase/php-jwt, atualmente na v7, em conformidade com o RFC 7519 e exigindo PHP 8.0 ou mais novo. Instale com o Composer:
composer require firebase/php-jwt
Então a API verifica primeiro e te entrega o payload só se a assinatura conferir:
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); // uma propriedade, e só depois da assinatura ter conferido
} catch (UnexpectedValueException $e) {
// token malformado, assinatura inválida ou claims expirados
}
Uma nota de versão: a v7 da biblioteca aplica comprimentos mínimos de chave para os algoritmos HMAC, então um segredo HS256 com menos de 32 bytes é rejeitado com um DomainException antes da assinatura ser verificada. Mantenha seus segredos longos; a biblioteca não deixa você esquecer.
Olhe a ordem nessa API: o JWT::decode() lança em caso de assinatura inválida, token expirado ou algoritmo ausente, em vez de retornar lixo, então um payload que você recebe de volta é um payload em que dá para confiar. A versão feita na mão acima é para entender, e para espiar tokens que não foram feitos para você; a biblioteca é para confiar.
HTTP Basic Auth, o header mais antigo
O header de autenticação mais antigo da web ainda anda de Base64. Segundo o RFC 7617, uma requisição HTTP Basic envia Authorization: Basic seguido da codificação Base64 de username:password. O RFC é explícito: isso é codificação, não proteção: qualquer um com uma captura de pacotes decodifica as duas metades numa tecla. O seu trabalho no lado da decodificação é parsear o header, decodificar em strict e comparar com uma função segura contra timing.
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])
) {
// autenticado
}
Dois detalhes mantêm isso seguro. O limite de 2 no explode() importa porque uma senha pode legalmente conter dois-pontos, e a comparação deve ser hash_equals(), nunca ==, para que um atacante não consiga usar timing para atravessar a sua lista de usuários. E sirva isso apenas sobre HTTPS; numa conexão comum, a camada Base64 é só enfeite.
E-mail, onde tudo começou
O Base64 nasceu para um problema específico: o transporte de e-mail só carregava ASCII de 7 bits, e as pessoas queriam enviar binários. O padrão MIME (RFC 2045, seção 6.8) fez do Base64 uma das codificações de transferência binária e adicionou duas regras da casa. Primeira: linhas codificadas não devem passar de 76 caracteres. Segunda: softwares de decodificação devem ignorar todo caractere fora do alfabeto, quebras de linha incluídas. Essa segunda regra é exatamente o motivo pelo qual o decodificador do PHP, em qualquer humor, devora um payload embrulhado em CRLF sem nenhum pré-processamento seu. (Também é disso que vem a tolerância a \r\n que você viu na tabela do modo strict acima.)
$png = "\x89PNG\r\n\x1a\n" . random_bytes(256);
$wrapped = chunk_split(base64_encode($png), 76, "\r\n");
// depois, no lado receptor, sem nenhuma limpeza:
$decoded = base64_decode($wrapped, true);
var_dump($decoded === $png); // bool(true): cada byte fez a ida e volta
Dois avisos práticos. Primeiro, o embrulho adiciona peso: com um CRLF a cada 76 caracteres, um anexo de 100 KB chega como cerca de 137 KB de texto (o fator quatro-terços de sempre, mais o overhead de quebras de linha). Segundo, para e-mail de verdade com headers, múltiplas partes e primos quoted-printable, a extensão opcional mailparse disseca mensagens RFC 822 completas parte por parte; para um único anexo conhecido, decodificação strict é tudo o que você precisa.
Armadura PEM: chaves e certificados
Certificados e chaves viajam em armadura PEM: um rótulo BEGIN, um bloco de Base64 em linhas de 64 caracteres e um rótulo END. O comprimento de linha de 64 caracteres é uma convenção herdada da especificação original Privacy Enhanced Mail (RFC 1421), e as ferramentas OpenSSL esperam isso, então importa quando você re-embala. Quando você decodifica, não importa nada: o decodificador simplesmente ignora as quebras de linha.
$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) {
// afinal não é Base64
}
var_dump($label); // string(11) "PRIVATE KEY"
Os bytes decodificados são DER, uma serialização binária compacta, e é com isso que as funções openssl_* trabalham no final. A backreference \1 na regex é o herói silencioso: ela garante que o rótulo END corresponda ao rótulo BEGIN, e é assim que você evita costurar o END de um certificado no BEGIN de uma chave quando um arquivo contém vários blocos.
Streams e payloads grandes
Decodificar é a direção que te ajuda: a saída tem três quartos do tamanho da entrada, então pressão de memória vinda do Base64 é rara. Ainda assim, quando um arquivo .b64 de centenas de megabytes cai no disco, você tem duas ferramentas para manter a pegada plana.
A primeira é decodificação em chunks. Divida a entrada limpa em pedaços cujo comprimento é um múltiplo de quatro caracteres, decodifique cada pedaço em strict e concatene. Cada chunk é um payload válido autocontido, então nada se perde nas bordas, e um arquivo corrompido falha rápido com um offset que você pode reportar.
$clean = str_replace(["\r", "\n"], '', file_get_contents('/var/www/uploads/huge.b64'));
$decoded = '';
$chunkSize = 4 * 50000; // um múltiplo de quatro caracteres, cerca de 150 KB de saída por chamada
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;
}
Um megabyte de Base64 decodifica em bem menos de um milissegundo em hardware moderno, então este loop custa quase nada; escolha-o pelos seus poderes de validação e reporte, não pela velocidade.
A segunda ferramenta é cidadã do mundo dos streams: o stream filter convert.base64-decode. Ele funciona em qualquer stream do PHP, então você pode decodificar direto de um ponteiro de arquivo, de php://input ou de um stream de memória sem nunca segurar o texto codificado inteiro numa variável. Como a função tolerante, ele simplesmente pula todo caractere fora do 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);
Qual ferramenta você escolhe? O filter quando os dados fluem por um stream e você quer que o PHP lide com a canalização; o loop de chunks quando precisa de validação por chunk, reporte de progresso ou do offset da corrupção.
Bancos de dados, arquivos de configuração e variáveis de ambiente
Base64 é um contêiner de texto, e é por isso que ele aparece em lugares onde você não esperaria. Em bancos de dados, um blob binário (um arquivo, um ícone, uma estrutura serializada) pode morar numa coluna TEXT como Base64, sobrevivendo a toda ferramenta que assume texto. Espere o valor armazenado ser cerca de 33 por cento maior que o original, e dimensione suas colunas conforme. Em arquivos de configuração e variáveis de ambiente, o Base64 é o truque para contrabandear valores que quebrariam o formato: um DSN de banco com ponto-e-vírgulas, uma senha com aspas, um valor com quebra de linha.
// .env ou config, escrito pela pessoa de ops:
// DB_DSN_B64 = cGc6aG9zdD1kYjtwYXNzd29yZD1xdSJvdGU=
$dsn = base64_decode(getenv('DB_DSN_B64') ?: '', true);
if ($dsn === false) {
exit('DB_DSN_B64 is not valid Base64.');
}
// o $dsn agora é: pg:host=db;password=qu"ote
O mesmo cuidado se aplica duas vezes aqui. Primeiro, isso é segurança de formato, não segredo: no momento em que um desenvolvedor lê o arquivo de configuração, ele decodifica o valor numa chamada. Nunca armazene um segredo como Base64 e chame isso de criptografia. Segundo, valide na inicialização: um valor de env corrompido ou colado pela metade é um false da chamada strict, e uma verificação de uma linha transforma um erro de runtime obscuro numa mensagem de inicialização acionável.
Da linha de comando
Nem toda decodificação acontece dentro de uma requisição web. Scripts de CLI, jobs de cron e one-liners decodificam Base64 o tempo todo, e a linha de comando é onde a função encontra o php://stdin:
php -r 'fwrite(STDOUT, base64_decode(file_get_contents("php://stdin"), true));' < payload.b64 > restored.bin
O shell já tem sua própria utilidade Base64 (o base64 -d do coreutils), e ele serve para trabalho rápido; o one-liner do PHP é para quando o próximo passo é lógica PHP: escrever num banco de dados, chamar uma API, rodar uma validação. Duas pegadinhas específicas do shell. A saída de uma decodificação são bytes crus, então mande para um arquivo ou para um comando que entende bytes, não para um terminal que vai deformá-los. E mantenha a flag strict ligado no one-liner, porque um colado truncado num terminal merece um false, não três bytes de lixo.
Armadilhas com sotaque PHP
Um tour rápido pelas armadilhas específicas do PHP, reunidas num lugar só:
- O padrão tolerante é o grande.
base64_decode('V@hpcy')devolve três bytes de lixo sem aviso, então todo decodificador de entrada não confiável precisa da flag strict e de um cheque defalse. - Um único caractere decodifica para uma string vazia em modo tolerante, e o mesmo vale para uma string de apenas espaços. Um resultado vazio prova quase nada; apenas o
falsesignifica falha, e você só o obtém em modo strict. - O
+numa query string já é um espaço antes do PHP vê-lo. Se um cliente envia?token=abc+defsem percent-encodá-lo, o PHP te entregaabc def(esse é comportamento de form-encoding, compartilhado porparse_str()eurldecode()), e nenhuma quantidade de mágica de decodificação traz o mais de volta. Base64 URL-safe (sem nenhum mais) é a correção para tokens em URLs. - Padding em falta é completado por você, em silêncio. Sete caracteres decodificam como oito; isso é conveniente, mas significa que um payload truncado por um pad ou dois ainda pode decodificar sem reclamar, então uma decodificação limpa nunca prova de todo que o payload chegou inteiro (os codificadores raw do Go e o Java são igualmente permissivos).
- O fantasma do
mbstring.func_overload. A configuração deprecada há muito que reescreviastrlen()e amigos para contar caracteres (removida no PHP 8.0) costumava quebrar a conta de bytes do Base64 em strings UTF-8. Código legado que você herdar pode ainda carregar comentários e workarounds para isso. Apague-os. - Bytes decodificados não são uma string UTF-8. Rodar
preg_match()com a flag/uoumb_substr()em binário decodificado é uma fonte instantânea de erros de "entrada malformada". Fareje primeiro, depois decida. - Passar
nullestá deprecado desde o PHP 8.1. Se uma variável pode ser null, coalesça-a para''antes da chamada. $_GETe amigos são decodificados com regras de form, não de URL. Se um valor chegou percent-encoded, orawurldecode()é o inverso mais seguro, porque deixa o+em paz.
Uma breve história de base64_decode
O Base64 em si é mais velho que a maior parte da web moderna (o padrão que o rege, o RFC 4648, é de 2006, e ele codificou a codificação MIME de 1996, que por sua vez desce da armadura PEM do início dos anos 1990). A história do PHP é seu próprio changelogzinho.
O PHP 4 entregou o base64_decode() como função de núcleo sem opções e sem modo strict; o humor tolerante era o único humor, e não havia maneira de pedir ao decodificador que reclamasse. O PHP 5.2.0, em novembro de 2006, adicionou a flag $strict, e a entrada do changelog vale a leitura: ele foi adicionado para impor a conformidade com o RFC 3548, o antecessor do RFC 4648 de hoje. Aquele flag acabou sendo a adição mais útil da vida da função.
Depois vieram os anos de debug. O PHP 5.3 corrigiu uma sequência de bugs de modo strict em duas point releases: bug #52327 (padding inicial tratado incorretamente em modo strict, corrigido no 5.3.4) e bug #55273 (espaço em branco após padding rejeitado em modo strict, corrigido no 5.3.9). (Uma correção de 2016 para integer-overflow também está arquivada sob o nome desta função: bug #72836, oficialmente intitulado "overflow de inteiro em base64_decode causou corrupção de heap" e corrigido no 5.6.25, mas o próprio código de reprodução do relatório de bug e a função corrigida mostram que o overflow real estava no cálculo de comprimento do base64_encode(), não no decodificador; o título é um nome errado herdado do relatório original.) Cada correção apertou o comportamento que você vê na tabela acima. O PHP 8.0 deu às duas funções Base64 tipos nativos de parâmetro e retorno, a assinatura que você viu no topo deste artigo, e a mesma linha de release removeu o mbstring.func_overload, a configuração que vinha quebrando a conta de bytes em silêncio há anos. O PHP 8.1 deprecou passar null para elas. Desde então, a superfície está congelada: um parâmetro, um flag, um tipo de retorno, sem mudanças.
Algumas delícias nerd
Como esta é uma referência longa, aqui vão alguns fatos específicos do PHP que são simplesmente divertidos:
- A identidade vazia.
base64_encode('')ebase64_decode('')são ambos''. As funções tratam o vazio como valor de primeira classe nas duas direções, semfalseenvolvido. - Um endereço estranho. No manual do PHP, as duas funções Base64 moram no capítulo "URLs" do livro "Outras Extensões Básicas". Não há um capítulo dedicado de "encoding"; é lá que você vai encontrá-las, no topo da lista daquele capítulo, à frente de
parse_url()e seus amigos. - O decodificador é um homomorfismo. Uma nota clássica de usuário do php.net observa que a função é um homomorfismo entre strings segmentadas em múltiplos de 4 e múltiplos de 3, que é a maneira formal de dizer que qualquer divisão em múltiplos de quatro é uma divisão válida. É por isso que a seção de decodificação em chunks funciona, e por que um arquivo de 1 MB pode ser decodificado em fatias de 50 KB com perda zero.
- Um parâmetro, um flag. Em mais de vinte anos, o
base64_decode()ganhou exatamente um parâmetro ($strict) e obase64_encode()não ganhou nenhum. - Ele tem irmãos mais velhos. A mesma extensão de núcleo também carrega
convert_uuencode()econvert_uudecode()(listadas sob String Functions no manual), as relíquias da era do discado quando o uuencode era o transporte binário da vez. Você quase nunca vai precisar deles, mas se um arquivo.uuantigo um dia cair na sua caixa de entrada, o PHP consegue abri-lo. - O modo strict mantém uma porta aberta para o e-mail. Os quatro caracteres de espaço em branco (espaço, tabulação, carriage return e line feed) navegam pelo modo strict de propósito, então um anexo embrulhado em MIME não precisa de pré-processamento. Tudo o mais, bytes NUL incluídos, é um
false.
A outra direção
Esse é o lado do decodificador, e é onde mora a maior parte da dor, porque decodificar é onde você encontra os dados dos outros: as escolhas de padding deles, as quebras de linha deles, os charsets deles, os tokens deles. A outra direção, transformar bytes numa string Base64 com o base64_encode(), é um bicho mais calmo: ele nunca falha, não tem modo strict, e o conjunto de armadilhas dele (codificação em dobro, quebras de linha erradas, a conta de tamanho) ganha o próprio guia. Base64 encoding em PHP, vinculado a partir desta página, cobre o codificador na mesma profundidade.
Última atualização: 2026-09-08
Artigo relacionado: Codificação Base64 em PHP: um guia completo