Base64 y codificación URL: propósito, trampas, uso correcto
Resumen (TL;DR)
Ejecuta echo -n "Hi" | base64 y obtienes exactamente cuatro caracteres de vuelta: SGk=. Dos bytes de entrada (16 bits) se cortan en trozos de 6 bits, se rellenan hasta una salida de 4 caracteres, y esa es toda la aritmética. Toma esa regla simple como un valor por defecto de “envolver todo para ir seguro” y el costo se acumula rápido: tres bytes de entrada siempre se convierten en cuatro caracteres de salida, lo que es aproximadamente un 33% de inflación, y un JPEG de 1,2 MB que vi una vez a un servicio envolver en Base64 dentro de un campo JSON aterrizó en aproximadamente 1,64 MB en el cable. La codificación no es cifrado, y no es compresión. Es un conjunto de reglas para expresar un alfabeto dentro de otro para que los datos sobrevivan a un canal que de otro modo los mutilaría. Base64 (RFC 4648 §4) existe para enviar bytes arbitrarios a través de sistemas que solo entienden texto: cuerpos de correo, campos de cadena JSON, datos de imagen en línea. Base64URL (el mismo RFC, §5) intercambia dos caracteres (+→-, /→_) para que el resultado sea seguro de incrustar en una ruta URL, un nombre de archivo o un JWT. La codificación porcentual (RFC 3986) es una herramienta aparte: convierte caracteres que significan algo para las URL (?, &, #, espacio, Unicode) en secuencias %XX que no. Elige la codificación que coincida con el canal: binario si el camino lo soporta, Base64 para canales de texto, Base64URL para datos embebidos en URL y codificación porcentual para cualquier cosa ya en un contexto URL. Las cadenas Base64 reveladas permanecen legibles para cualquiera con un decodificador, así que nunca trates la codificación como un mecanismo de secreto.
Antecedentes y conceptos
Los canales solo de texto existían mucho antes de que nadie les pidiera transportar imágenes. El correo electrónico históricamente asumía ASCII de 7 bits y corrompía o descartaba bytes con el bit alto activado; las cabeceras HTTP aún restringen ciertos caracteres; los campos de cadena JSON son UTF-8 pero no pueden contener bytes de control crudos (0x00–0x1F) de forma segura. Base64, estandarizado en RFC 4648, resuelve el problema mapeando cada 3 bytes de entrada (24 bits) a 4 caracteres de salida de 6 bits cada uno. El alfabeto es de 64 caracteres: A-Z (índices 0–25), a-z (26–51), 0-9 (52–61), + (62), / (63), con = como relleno para mantener la longitud de salida múltiplo de cuatro. Un byte de entrada produce XX== (8 bits → 6+2 rellenado), dos bytes producen XXX= (16 bits → 6+6+4 rellenado), y tres bytes producen XXXX sin relleno.
La variante URL-safe de Base64 solo cambia los índices 62 y 63 del alfabeto: + se convierte en -, / en _, porque + se decodifica como un espacio en envíos de formularios legacy y / es un separador de ruta. En muchos contextos URL-safe el relleno = también se elimina; esta es la forma usada en JSON Web Tokens (RFC 7519), donde un triple compacto cabecera.payload.firma debe caber en una cabecera Authorization: Bearer ... sin reescapar.
La codificación porcentual (a veces llamada URL-encoding) es aparte. RFC 3986 define dos categorías de ASCII: caracteres no reservados (A-Z, a-z, 0-9, -, ., _, ~) que pueden aparecer literalmente, y caracteres reservados (:, /, ?, #, [, ], @, !, $, &, ', (, ), *, +, ,, ;, =) que significan algo estructuralmente y deben codificarse cuando aparecen como datos en vez de sintaxis. Cualquier byte fuera de ese conjunto —incluidos los bytes de un carácter no-ASCII codificado en UTF-8— se escribe como %XX en hexadecimal. encodeURIComponent en JavaScript codifica tanto caracteres reservados como no reservados de forma agresiva para uso dentro de un componente; encodeURI es más permisivo porque asume que pasas una URL entera.
Una consecuencia sutil de estas tres codificaciones es que no son intercambiables, incluso cuando los bytes que producen casualmente se solapen. Un parámetro de consulta codificado porcentualmente sigue siendo caracteres ASCII codificando otros caracteres ASCII; pasarlo por un decodificador Base64 produce basura. Una cadena Base64 estándar colocada en una ruta URL se rompe silenciosamente porque el / dentro se lee como separador de ruta; he visto a un colega perder medio día por un solo carácter de ese tipo. Esas confusiones son la fuente de la mitad de los bugs de codificación en producción.
Comparación y datos
| Propiedad | Base64 (estándar) | Base64URL | Codificación porcentual |
|---|---|---|---|
| Cuándo usarla | Canal de texto que rechaza binario crudo (MIME de correo, cadena JSON de bytes) | Igual, pero embebida en URL, nombre de archivo o JWT | Un componente URL único que contiene caracteres reservados o no ASCII |
| Alfabeto | A–Z a–z 0–9 + / = | A–Z a–z 0–9 - _ (relleno opcional) | Conjunto no reservado; todo lo demás se convierte en %XX |
| Overhead | Aprox. 33% (4 caracteres por 3 bytes), más wrap de 76 caracteres por línea en MIME | Aprox. 33%, sin relleno en uso típico | Varía: 1 byte de UTF-8 se convierte en 3 bytes ASCII (%XX) |
| Se rompe por | + y / crudos en URL, longitud sin relleno en decodificadores estrictos | Decodificadores Base64 estándar que requieren +/= | Doble codificación (datos ya codificados porcentualmente se codifican de nuevo) |
| Usos comunes | Adjuntos MIME, URIs de datos, binarios en línea en JSON | JWT, IDs URL-safe, enlaces cortos | Parámetros de consulta, segmentos de ruta, cuerpos de formulario |
Los números importan para presupuestar. Una imagen de 1 MB codificada en Base64 se vuelve aproximadamente 1,37 MB; con el wrap de 76 caracteres por línea de MIME, añade otro 2% o así. En una respuesta HTTP esa inflación de tamaño golpea tanto al servidor como al parser del cliente. La codificación porcentual suele ser un problema menor a nivel de cadena pero puede multiplicar bytes para texto CJK: un carácter coreano en UTF-8 ocupa 3 bytes, que se convierten en 9 caracteres ASCII tras codificar. La palabra de dos caracteres “안녕” se convierte en la secuencia de 18 bytes %EC%95%88%EB%85%95 dentro de una URL.
Escenarios reales
Escenario 1 — Adjuntos de correo. Un PDF viaja dentro de una parte MIME con Content-Transfer-Encoding: base64. El cliente de correo codifica el archivo en Base64, envuelve líneas a 76 caracteres (el límite MIME clásico) y lo envía. El receptor invierte el proceso. La asunción solo-texto de SMTP hizo de Base64 el valor por defecto aquí mucho antes de que existieran extensiones modernas como 8BITMIME o BINARYMIME, y la mayoría de servidores de correo aún emiten Base64 por seguridad. El overhead del 33% se acepta como el costo de entrega fiable.
Escenario 2 — JSON Web Tokens. Un JWT (RFC 7519) es cabecera.payload.firma, donde cada segmento es JSON o bytes codificados en Base64URL, sin relleno. El alfabeto URL-safe significa que los tokens pueden aparecer en cabeceras Authorization, parámetros de consulta access_token y líneas de log sin reescapar. La ausencia de relleno los mantiene cortos. Una advertencia: cualquiera que decodifique un JWT puede leer los claims; la codificación no es seguridad; la firma HMAC o RSA sí lo es. No pongas secretos en el payload.
Escenario 3 — URIs de datos para imágenes pequeñas. Un background-image: url("data:image/png;base64,iVBORw0K...") incrusta un PNG directamente en CSS. Esto evita una ida y vuelta para assets diminutos como iconos. Pero el overhead del 33% de Base64 y la pérdida de caché del navegador y peticiones paralelas significan que solo es una ganancia para assets lo suficientemente pequeños como para que la ida y vuelta HTTP extra hubiera costado más. En mi experiencia el punto de equilibrio está en torno a 1–2 KB; por encima, un archivo separado cacheado o un SVG suele ser más rápido.
Escenario 4 — Subir un avatar de usuario. Un antipatrón común es leer un archivo con FileReader.readAsDataURL, que devuelve una cadena data:image/png;base64,..., y luego hacer POST de esa cadena como campo JSON. Funciona, pero casi nunca es la forma correcta: el payload ahora es 33% más grande, el servidor tiene que decodificarlo antes de escribir a disco, y ambos lados queman memoria en la forma de cadena. En un caso que observé, una imagen de 5 MB se infló a aproximadamente 6,7 MB de JSON y causó timeouts en redes móviles; cambiar a multipart/form-data envió el mismo archivo a 5 MB. Recurre a Base64 en este flujo solo cuando la capa de transporte realmente requiera una cadena JSON, como cuando una API de terceros no acepta multipart.
Errores comunes
“Base64 es cifrado.” No lo es. El mapeo está publicado en RFC 4648, el alfabeto es fijo, y cualquier decodificador devuelve los bytes originales. La codificación protege el transporte, no el contenido. Si el payload es sensible, cifra primero y codifica en Base64 el texto cifrado para transporte.
“La codificación URL solo hace falta para caracteres no ASCII.” Muchos caracteres ASCII reservados también tienen que codificarse cuando aparecen como datos. Un valor de consulta que contenga & debe volverse %26, o el servidor lo parseará como nuevo parámetro. # debe volverse %23, o todo lo que va después se trata como fragmento. La regla es estructural, no basada en conjunto de caracteres.
“Todo lo que pasa por HTTP debería codificarse en Base64 por seguridad.” HTTP transporta cuerpos binarios alegremente: Content-Type: application/octet-stream, transferencia chunked, cualquier valor de byte. Base64 es un workaround para canales que no lo hacen, y pagar un 33% de overhead cuando el canal ya maneja bytes es puro impuesto. Usa multipart/form-data o un cuerpo crudo para subidas de archivo, y reserva Base64 para los casos donde el canal realmente necesita texto (un campo JSON, un parámetro URL, un cuerpo MIME).
“Base64 y Base64URL son intercambiables.” No lo son. Pasar una cadena URL-safe a un decodificador Base64 estricto que espera +/ fallará o producirá basura. Las librerías suelen proporcionar ambos; empareja el codificador con el decodificador de extremo a extremo. Buffer.from(s, 'base64') de Node acepta cualquier alfabeto, pero no toda librería estándar es tan tolerante.
Lista de verificación
- ¿El canal es solo de 7 bits o texto estructurado? Cuerpo de correo, cadena JSON de bytes, URI
data:de CSS → Base64. - ¿El valor codificado va a una URL, nombre de archivo o JWT? Usa Base64URL y decide si se permite relleno.
- ¿El valor ya es un componente URL? Codifica porcentualmente, y solo las partes que lo necesitan.
- ¿El dato es grande? Considera si el canal realmente requiere codificación; un cuerpo binario crudo evita el 33% de overhead.
- ¿La decodificación es estricta o permisiva? Empareja alfabetos estándar/URL-safe de extremo a extremo; no los mezcles.
- ¿La confidencialidad es un objetivo? Cifra antes de codificar. La codificación por sí sola nunca hace los datos secretos.
Herramienta relacionada
El codificador/decodificador Base64 de Patrache Studio maneja variantes estándar y URL-safe localmente, así que los bytes de entrada no salen de tu navegador; útil cuando el dato es un token o una clave pequeña. Si el payload Base64 resulta ser un JWT, combínalo con Formateo, validación y esquema JSON en la práctica para inspeccionar los claims decodificados limpiamente. Y si el blob codificado es un ID compacto derivado de un UUID, UUID v1 vs v4 vs v7: elegir una clave primaria de BD explica por qué los bytes originales que codificaste en Base64URL siguen importando para orden y indexación.
Referencias
- 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, entrada del glosario “Base64” — https://developer.mozilla.org/en-US/docs/Glossary/Base64
- MDN, “encodeURIComponent()” — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent