Base64 e codificação de URL: propósito, armadilhas e uso correto
Resumo (TL;DR)
Rode echo -n "Hi" | base64 e você recebe exatamente quatro caracteres: SGk=. Dois bytes de entrada (16 bits) são fatiados em pedaços de 6 bits, preenchidos até uma saída de 4 caracteres, e toda a aritmética termina aí. Trate essa regra simples como um padrão do tipo “embrulhar tudo por segurança” e o custo se acumula rápido — três bytes de entrada sempre viram quatro caracteres de saída, o que é aproximadamente 33% de inflação, e uma vez vi um JPEG de 1,2 MB que um serviço embrulhou em Base64 dentro de um campo JSON aterrissando em cerca de 1,64 MB na rede. Codificação não é criptografia, e não é compressão. É um conjunto de regras para expressar um alfabeto dentro de outro para que o dado sobreviva a um canal que de outro modo o estragaria. O Base64 (RFC 4648 §4) existe para enviar bytes arbitrários por sistemas que só entendem texto — corpos de e-mail, campos string de JSON, dados de imagem inline. O Base64URL (mesma RFC, §5) troca dois caracteres (+→-, /→_) para que o resultado seja seguro em caminhos de URL, nomes de arquivo ou JWTs. A percent-encoding (RFC 3986) é uma ferramenta separada: ela converte caracteres que significam algo para URLs (?, &, #, espaço, Unicode) em sequências %XX que não significam. Escolha a codificação que combina com o canal: binário se o caminho suporta, Base64 para canais de texto, Base64URL para dados embutidos em URL e percent-encoding para qualquer coisa já em contexto de URL. Strings Base64 reveladas permanecem legíveis para qualquer um com um decoder, então nunca trate codificação como mecanismo de sigilo.
Contexto e conceitos
Canais só-texto existiam muito antes de alguém pedir que carregassem imagens. E-mail historicamente assumia ASCII 7-bit e corrompia bytes com o bit alto ligado; cabeçalhos HTTP ainda restringem certos caracteres; campos string de JSON são UTF-8, mas não conseguem carregar bytes de controle crus (0x00–0x1F) com segurança. O Base64, padronizado na RFC 4648, resolve o problema mapeando a cada 3 bytes de entrada (24 bits) 4 caracteres de saída de 6 bits cada. O alfabeto tem 64 caracteres: A-Z (índices 0–25), a-z (26–51), 0-9 (52–61), + (62), / (63), com = como preenchimento para manter o comprimento da saída múltiplo de quatro. Um byte de entrada gera XX== (8 bits → 6+2 preenchidos), dois bytes geram XXX= (16 bits → 6+6+4 preenchidos), e três bytes geram XXXX sem preenchimento.
A variante URL-safe do Base64 só muda os índices 62 e 63 do alfabeto — + vira -, / vira _ — porque + é decodificado como espaço em formulários legados e / é separador de caminho. Em muitos contextos URL-safe, o preenchimento = também é descartado; é a forma usada em JSON Web Tokens (RFC 7519), em que um trio compacto header.payload.signature precisa caber em um cabeçalho Authorization: Bearer ... sem re-escape.
A percent-encoding (às vezes chamada de URL-encoding) é separada. A RFC 3986 define duas categorias de ASCII: caracteres não reservados (A-Z, a-z, 0-9, -, ., _, ~) que podem aparecer literalmente, e caracteres reservados (:, /, ?, #, [, ], @, !, $, &, ', (, ), *, +, ,, ;, =) que significam algo estruturalmente e precisam ser codificados quando aparecem como dado em vez de sintaxe. Qualquer byte fora desse conjunto — incluindo os bytes de um caractere não-ASCII codificado em UTF-8 — é escrito como %XX em hexadecimal. O encodeURIComponent do JavaScript codifica tanto caracteres reservados quanto não reservados de forma agressiva para uso dentro de um componente; encodeURI é mais permissivo porque assume que você está passando uma URL inteira.
Uma consequência sutil dessas três codificações é que elas não são intercambiáveis, mesmo quando os bytes que produzem parecem se sobrepor. Um parâmetro de consulta percent-encoded ainda é caracteres ASCII codificando outros caracteres ASCII; passá-lo por um decoder Base64 produz lixo. Uma string Base64 padrão largada em um caminho de URL silenciosamente quebra porque o / lá dentro é lido como separador de caminho — já vi um colega perder metade de um dia por um único caractere desses. Essas confusões são a origem de metade dos bugs de codificação em produção.
Comparação e dados
| Propriedade | Base64 (padrão) | Base64URL | Percent-encoding |
|---|---|---|---|
| Quando usar | Canal de texto que rejeita binário cru (MIME de e-mail, string de bytes em JSON) | Mesmo, mas embutido em URL, nome de arquivo ou JWT | Um único componente de URL contendo caracteres reservados ou não-ASCII |
| Alfabeto | A–Z a–z 0–9 + / = | A–Z a–z 0–9 - _ (padding opcional) | Conjunto não reservado; todo o resto vira %XX |
| Overhead | Cerca de 33% (4 chars por 3 bytes), mais quebras de linha a cada 76 caracteres em MIME | Cerca de 33%, sem padding no uso típico | Varia — 1 byte de UTF-8 vira 3 bytes ASCII (%XX) |
| Quebra em | + e / crus em URLs, comprimento sem padding em decoders estritos | Decoders Base64 padrão que exigem +/= | Dupla codificação (dado já percent-encoded sendo codificado de novo) |
| Usos comuns | Anexos MIME, data URIs, binários inline em JSON | JWT, IDs URL-safe, links curtos | Parâmetros de query, segmentos de caminho, corpos de formulário |
Os números importam para orçamento. Uma imagem de 1 MB codificada em Base64 vira cerca de 1,37 MB; com as quebras de linha a cada 76 caracteres do MIME, adicione mais uns 2%. Em uma resposta HTTP, essa inflação de tamanho atinge tanto o servidor quanto o parser do cliente. A percent-encoding costuma ser um problema menor no nível de string, mas pode multiplicar bytes para texto CJK: um caractere coreano em UTF-8 ocupa 3 bytes, que viram 9 caracteres ASCII após a codificação. A palavra de dois caracteres “안녕” se transforma na sequência de 18 bytes %EC%95%88%EB%85%95 dentro de uma URL.
Cenários reais
Cenário 1 — Anexos de e-mail. Um PDF viaja dentro de uma parte MIME com Content-Transfer-Encoding: base64. O cliente de e-mail codifica o arquivo em Base64, quebra linhas a cada 76 caracteres (o limite clássico do MIME) e envia. O receptor faz o processo inverso. A premissa só-texto do SMTP fez do Base64 o padrão aqui muito antes de extensões modernas como 8BITMIME ou BINARYMIME existirem, e a maioria dos servidores de e-mail ainda emite Base64 por segurança. O overhead de 33% é aceito como custo da entrega confiável.
Cenário 2 — JSON Web Tokens. Um JWT (RFC 7519) é header.payload.signature, em que cada segmento é JSON ou bytes codificados em Base64URL, sem padding. O alfabeto URL-safe permite que tokens apareçam em cabeçalhos Authorization, parâmetros access_token e linhas de log sem re-escape. A ausência de padding os mantém curtos. Um alerta: qualquer um decodificando um JWT pode ler as claims — a codificação não é segurança; a assinatura HMAC ou RSA é. Não coloque segredos no payload.
Cenário 3 — Data URIs para imagens pequenas. Um background-image: url("data:image/png;base64,iVBORw0K...") embute um PNG diretamente em CSS. Isso evita uma ida e volta para ativos minúsculos como ícones. Mas o overhead de 33% do Base64 e a perda de cache do navegador e de requisições paralelas significam que só vale a pena para ativos pequenos o bastante para que a requisição HTTP extra teria custado mais. Na minha experiência, o ponto de equilíbrio fica em torno de 1–2 KB; acima disso, um arquivo separado em cache ou um SVG costuma ser mais rápido.
Cenário 4 — Upload de avatar de usuário. Um antipadrão comum é ler um arquivo com FileReader.readAsDataURL, que retorna uma string data:image/png;base64,..., e depois fazer POST dessa string como um campo JSON. Funciona, mas quase nunca é a forma certa: o payload agora é 33% maior, o servidor precisa decodificar antes de gravar em disco, e os dois lados queimam memória com a forma string. Em um caso que observei, uma imagem de 5 MB inchou para cerca de 6,7 MB de JSON e causou timeouts em redes móveis; mudar para multipart/form-data enviou o mesmo arquivo em 5 MB. Recorra ao Base64 nesse fluxo apenas quando o transporte realmente exigir uma string JSON, como quando uma API de terceiros não aceita multipart.
Equívocos comuns
“Base64 é criptografia.” Não é. O mapeamento está publicado na RFC 4648, o alfabeto é fixo e qualquer decoder devolve os bytes originais. A codificação protege o transporte, não o conteúdo. Se o payload é sensível, criptografe primeiro e faça Base64 do ciphertext para transporte.
“URL encoding só é necessária para caracteres não-ASCII.” Muitos caracteres ASCII reservados também precisam ser codificados quando aparecem como dado. Um valor de query contendo & precisa virar %26, ou o servidor vai interpretar como um novo parâmetro. # precisa virar %23, ou tudo depois dele é tratado como fragmento. A regra é estrutural, não baseada no conjunto de caracteres.
“Tudo que vai por HTTP deveria ser codificado em Base64 por segurança.” HTTP carrega corpos binários sem problema — Content-Type: application/octet-stream, transferência em chunks, qualquer valor de byte. Base64 é uma gambiarra para canais que não carregam, e pagar 33% de overhead quando o canal já lida com bytes é imposto puro. Use multipart/form-data ou corpo cru para uploads de arquivo, e reserve Base64 para os casos em que o canal genuinamente precisa de texto (um campo JSON, um parâmetro de URL, um corpo MIME).
“Base64 e Base64URL são intercambiáveis.” Não são. Jogar uma string URL-safe em um decoder Base64 estrito que espera +/ vai falhar ou produzir lixo. Bibliotecas normalmente oferecem os dois; alinhe o encoder com o decoder de ponta a ponta. O Buffer.from(s, 'base64') do Node aceita os dois alfabetos, mas nem toda biblioteca padrão é tão tolerante.
Checklist
- O canal é só 7-bit ou texto estruturado? Corpo de e-mail, string de bytes em JSON, URI
data:em CSS → Base64. - O valor codificado vai para uma URL, nome de arquivo ou JWT? Use Base64URL e decida se o padding é permitido.
- O valor já é um componente de URL? Faça percent-encoding, e só das partes que precisam.
- O dado é grande? Considere se o canal realmente exige codificação — um corpo binário cru evita 33% de overhead.
- A decodificação é estrita ou permissiva? Alinhe alfabetos padrão/URL-safe de ponta a ponta; não misture.
- Confidencialidade é um objetivo? Criptografe antes de codificar. Codificar sozinho nunca torna o dado secreto.
Ferramenta relacionada
O encoder/decoder Base64 da Patrache Studio lida com a variante padrão e a URL-safe localmente, então os bytes de entrada não saem do seu navegador — útil quando o dado é um token ou uma chave pequena. Se o payload Base64 acontece ser um JWT, combine com Formatação, validação e JSON Schema na prática para inspecionar as claims decodificadas de forma limpa. E se o blob codificado é um ID compacto derivado de um UUID, UUID v1 vs v4 vs v7: escolhendo uma chave primária de BD explica por que os bytes originais que você codificou em Base64URL continuam importando para ordem de classificação e indexação.
Referências
- 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, verbete “Base64” do glossário — https://developer.mozilla.org/en-US/docs/Glossary/Base64
- MDN, “encodeURIComponent()” — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent