"Podemos codificarlo en Base64 antes de guardarlo." Si revisas suficientes ficheros de config, paneles internos, bundles de navegador, handlers de webhooks o código de autenticación, tarde o temprano oyes esa frase. Suena plausible porque la salida parece revuelta:
c2VjcmV0IHBhc3N3b3Jk
Pero Base64 no es cifrado. No es protección con contraseña. No es una forma segura de esconder claves de API, claims de JWT, IDs de usuario, credenciales de Basic Auth ni datos privados en una URL. El mismo valor se decodifica en una sola llamada:
atob("c2VjcmV0IHBhc3N3b3Jk");
// "secret password"
La regla práctica es directa: si el valor original vuelve sin ninguna clave secreta, Base64 no lo protegió. Solo cambió la representación.
El test rápido de seguridad
Cuando veas Base64 en el código, pregúntate qué trabajo está haciendo:
| Base64 se está usando para... | ¿Buen uso? | Qué revisar |
|---|---|---|
| Meter bytes en JSON | Sí | Mantener límites de tamaño razonables |
Incrustar una data: URI pequeña |
Sí | No incluir archivos grandes a ciegas |
| Guardar como texto ciphertext, nonce, salt o bytes de firma | Sí | El paso criptográfico debe ir antes de Base64 |
| Llevar un cursor de paginación opaco | Normalmente | Trátalo como detalle de interfaz, no como control de acceso |
| Construir una cabecera Basic Auth | Solo con HTTPS | TLS protege la petición, Base64 no |
| Ocultar una clave de API en el frontend | No | Todo navegador recibe el valor y el decodificador |
| Guardar una contraseña | No | Usa hashing lento de contraseñas |
| Ocultar claims de JWT | No | Los payloads JWT firmados son legibles salvo que se cifren como JWE |
| Ocultar IDs o roles de usuario en URLs | No | La autorización en servidor sigue siendo obligatoria |
Si la frase que tienes en la cabeza es "para que nadie lo lea", no estás describiendo Base64. Estás describiendo cifrado, hashing, firma, control de acceso o gestión de secretos.
Qué hace Base64 realmente
Base64 es un esquema de codificación binario a texto. RFC 4648 define el alfabeto Base64 habitual como:
A-Z a-z 0-9 + / =
El carácter = es padding. Base64 agrupa los bytes de entrada en bloques de 24 bits: 3 bytes de entrada se convierten en 4 caracteres imprimibles. Por eso la salida Base64 es aproximadamente un 33 % mayor que los bytes en crudo antes de comprimir.
btoa("hello");
// "aGVsbG8="
atob("aGVsbG8=");
// "hello"
El mismo viaje de ida y vuelta funciona desde una terminal:
printf '%s' 'secret password' | base64
# c2VjcmV0IHBhc3N3b3Jk
printf '%s' 'c2VjcmV0IHBhc3N3b3Jk' | base64 -d
# secret password
No hay prompt de contraseña, clave privada, paso de descifrado, salt, nonce, IV, tag ni factor de coste. Quien tenga la cadena codificada puede invertirla.
Base64 vs Base64url
El Base64 estándar no siempre es seguro para URLs porque +, / y = resultan incómodos en URLs, nombres de fichero, cookies y segmentos de token. Base64url cambia el alfabeto:
| Característica | Base64 estándar | Base64url |
|---|---|---|
| Caracteres 62 y 63 | + y / |
- y _ |
| Padding | Normalmente = |
Suele omitirse |
| Sitios habituales | MIME, PEM, Basic Auth, campos binarios | JWTs, tokens en URL, nombres de fichero |
| Diferencia de seguridad | Ninguna | Ninguna |
Esto importa cuando depuras JWTs. Un JWT usa Base64url, no Base64 estándar con padding:
header.payload.signature
Si pegas un segmento de JWT en un decodificador Base64 estándar y falla, la cadena no tiene por qué ser secreta ni estar cifrada. Puede que simplemente necesite tratamiento Base64url:
function base64urlToBase64(part) {
let out = part.replace(/-/g, "+").replace(/_/g, "/");
while (out.length % 4 !== 0) out += "=";
return out;
}
Esa conversión cambia el alfabeto. No añade confidencialidad.
UTF-8: la trampa del navegador
Los btoa() y atob() del navegador trabajan sobre cadenas binarias. Los ejemplos ASCII simples van bien:
btoa("hello");
// "aGVsbG8="
Pero el texto Unicode necesita un paso previo a bytes. Esto pilla por sorpresa a equipos que prueban con hello y más adelante codifican nombres de cliente, texto en chino, emoji o árabe:
btoa("你好");
// InvalidCharacterError in browsers
Usa bytes UTF-8:
function utf8ToBase64(text) {
const bytes = new TextEncoder().encode(text);
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
return btoa(binary);
}
function base64ToUtf8(base64) {
const binary = atob(base64);
const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
const encoded = utf8ToBase64("你好");
base64ToUtf8(encoded);
// "你好"
La herramienta Base64 Encode & Decode de este sitio hace el viaje UTF-8 en el navegador, así el texto no ASCII vuelve como texto y no como mojibake.
Por qué los desarrolladores confunden Base64 con cifrado
Base64 aparece a menudo cerca de seguridad real:
- Los certificados TLS se guardan en texto PEM.
- Los JWTs usan secciones Base64url.
- HTTP Basic Auth usa Base64 para
username:password. - Las claves públicas SSH contienen material de clave codificado en Base64.
- El ciphertext cifrado se suele codificar en Base64 antes de guardarlo o transportarlo.
Este último punto causa la mayor parte de la confusión. Los datos cifrados pueden ir codificados en Base64, pero Base64 no es el cifrado. La seguridad vino de una operación criptográfica como AES-GCM, XChaCha20-Poly1305, RSA-OAEP, JWE o TLS. Base64 solo hizo que los bytes resultantes fueran fáciles de guardar en JSON, correo, cabeceras o una columna de base de datos.
Usa palabras precisas en el código y en los textos de producto:
encodedBackup // OK si solo es Base64
encryptedBackup // Solo OK si hubo cifrado real
signedState // OK si un HMAC o firma protege la integridad
passwordHash // OK si lo produjo un algoritmo de hashing de contraseñas
Los nombres aquí no son cosmética. Un nombre engañoso puede hacer que un futuro revisor asuma que existe una frontera de seguridad cuando no la hay.
Codificación vs cifrado vs hashing vs firma
Estas operaciones resuelven problemas distintos:
| Operación | Trabajo principal | ¿Requiere secreto? | ¿Reversible? | Ejemplo |
|---|---|---|---|---|
| Codificación | Cambiar la representación | No | Sí | Base64, Base64url, URL encoding |
| Cifrado | Ocultar el texto claro | Sí | Sí, con clave | AES-GCM, XChaCha20-Poly1305, JWE |
| Hashing | Huella de una vía | No | No | SHA-256 para checksums |
| Hashing de contraseñas | Verificación lenta de contraseñas | Salt más parámetros de coste | No | Argon2id, bcrypt, scrypt |
| Firma / MAC | Probar integridad y origen | Sí, o par de claves | No | HMAC, JWS, firma Ed25519 |
Base64 responde a "¿puede esta secuencia de bytes viajar por un canal de texto?". No responde a "¿quién puede leer esto?", "¿quién cambió esto?" ni "¿tiene este usuario permiso para ese registro?".
Error real de seguridad 1: claves de API escondidas en el frontend
Este patrón sale en apps React, extensiones de navegador, temas de WordPress y herramientas de admin internas:
// La clave sigue llegando a cada navegador.
const apiKey = atob("c2stbGl2ZS1hYmMxMjM0NTY3ODk=");
fetch("https://api.example.com/report", {
headers: { Authorization: `Bearer ${apiKey}` },
});
Base64 no ayuda porque el navegador recibe tanto el valor codificado como el decodificador. Cualquiera puede abrir DevTools, inspeccionar el bundle JavaScript, ejecutar la misma llamada a atob() y copiar la clave.
El arreglo es arquitectónico:
- Mantén los secretos reales en el servidor.
- Da a las claves de navegador scopes estrechos y restricciones por dominio.
- Proxya las llamadas sensibles a través de tu backend.
- Rota cualquier secreto que ya haya llegado a los usuarios.
- Asume que los escáneres de secretos saben decodificar cadenas parecidas a Base64.
Codificar una clave puede complicar el code review. No hace la clave más segura.
Error real de seguridad 2: contraseñas guardadas como Base64
Esto es dañino porque puede quedarse en silencio en una base de datos durante años:
// Almacenamiento reversible. No hagas esto.
const storedPassword = btoa(password);
// Después:
const password = atob(storedPassword);
Si se filtra la base de datos, cada contraseña vuelve al instante. No hay coste de crackeo porque no hay nada que crackear.
El almacenamiento de contraseñas debe ser de una vía:
// Forma del flujo, usando una librería de hashing de contraseñas.
const hash = await argon2.hash(password);
const ok = await argon2.verify(hash, candidatePassword);
Los sistemas normales de login nunca deberían necesitar recuperar la contraseña del usuario. Solo necesitan verificar una contraseña candidata contra un hash guardado.
Error real de seguridad 3: Basic Auth confundido con cifrado
La cabecera parece lo bastante codificada como para engañar:
Authorization: Basic dXNlcjpwYXNzd29yZA==
Decodifica a:
atob("dXNlcjpwYXNzd29yZA==");
// "user:password"
HTTP Basic Auth solo es aceptable cuando la petición va protegida por HTTPS. TLS cifra el intercambio HTTP en tránsito. Base64 solo empaqueta username:password en una cadena ASCII apta para cabeceras.
Sobre HTTP plano, un observador de red puede leer las credenciales. En logs, proxies inversos, trazas APM y volcados de soporte, una cabecera Basic Auth debe tratarse como una contraseña en claro.
Error real de seguridad 4: payloads de JWT tratados como privados
Un JWT firmado normal tiene tres secciones Base64url:
header.payload.signature
Las dos primeras decodifican a JSON. La tercera es la firma.
eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0
Decodifica a:
{
"sub": "1234",
"role": "admin"
}
Eso no significa que el token sea falso. Un JWT firmado puede ser evidente a manipulación y a la vez legible. La firma protege la integridad: le dice al servidor si el token lo emitió una parte de confianza y si la cabecera o el payload cambiaron. No oculta el payload.
No metas contraseñas, claves de API, secretos de sesión, números completos de tarjeta, detalles médicos ni datos privados de perfil en un payload de JWT firmado normal. Si los claims necesitan confidencialidad, usa JWE o evita meter esos datos en el token.
Además: decodificar no es verificar. Una herramienta de dev puede mostrar claims; un servidor sigue teniendo que verificar la firma, el issuer, la audience, la expiración, el key ID, la política de algoritmo y las reglas de autorización propias de la app antes de confiar en ellos.
Error real de seguridad 5: parámetros de URL que parecen opacos
Las apps legacy suelen pasar estado así:
/profile?data=eyJ1c2VySWQiOjQyfQ==
Eso decodifica a:
{
"userId": 42
}
Salen dos problemas. Cualquiera puede leerlo y cualquiera puede cambiarlo y volverlo a codificar. Si /profile?data=... es lo único que decide qué registro de usuario cargar, el bug es autorización ausente.
Patrones más seguros:
- Trata los datos decodificados de la URL como input no confiable.
- Vuelve a comprobar los permisos en el servidor.
- Prefiere una referencia corta y aleatoria en el servidor cuando el estado sea sensible.
- Usa un HMAC o una firma si el cliente debe llevar estado evidente a manipulación.
- Mantén el estado privado fuera de las URLs, porque las URLs acaban copiadas en logs, historial de navegador, analítica y capturas de soporte.
Error real de seguridad 6: exports "cifrados" que solo están codificados
Otro olor de code review es una función así:
function exportEncryptedBackup(data) {
return btoa(JSON.stringify(data));
}
El nombre dice cifrado. El código dice codificado.
Si el export solo pretende ser un blob de texto portable, renómbralo:
function exportBase64Backup(data) {
return btoa(JSON.stringify(data));
}
Si a los usuarios se les está diciendo que el export está cifrado, usa cifrado autenticado real y haz explícita la gestión de claves. Eso significa una librería de cripto revisada, nonces/IVs aleatorios, tags de autenticación, generación segura de claves y un plan de almacenamiento de claves. Base64 puede envolver el ciphertext después.
Para qué sirve Base64 realmente
Base64 es útil. El error es asignarle un trabajo de seguridad.
Buenos usos:
- Datos binarios dentro de JSON, XML, HTML o correo.
- Adjuntos MIME de correo.
data:URIs pequeñas para imágenes o fuentes.- Formateo de salida criptográfica para ciphertext, firmas, nonces, salts y claves públicas.
- Segmentos de serialización compacta JWT y JOSE.
- Cursores de paginación opacos donde la legibilidad es asunto de producto, no una frontera de seguridad.
- Depuración de campos de API, payloads JWT y blobs de config.
La formulación segura es "codificado en Base64", no "cifrado". Esa sola palabra evita una cantidad sorprendente de malentendidos en producción.
Qué usar en su lugar
Si echaste mano de Base64 porque querías seguridad, elige la primitiva que encaje con el trabajo:
| Objetivo | Usa en su lugar |
|---|---|
| Ocultar datos a usuarios o atacantes | Cifrado autenticado como AES-GCM o XChaCha20-Poly1305 |
| Proteger datos en tránsito | HTTPS/TLS |
| Guardar contraseñas | Argon2id, bcrypt o scrypt con salt por contraseña y coste ajustado |
| Guardar claves de API | Gestor de secretos, entorno del servidor, scopes, logs de auditoría y rotación |
| Probar que un valor no cambió | HMAC o firma digital |
| Evitar que los usuarios editen estado en la URL | Autorización en servidor más estado firmado o referencia de sesión en servidor |
| Mantener privados los claims de un JWT | JWE, o no meter claims privados en el token |
| Meter binarios en JSON, correo o una cabecera | Base64 es lo correcto |
La parte difícil rara vez es el nombre del algoritmo. Es modelar amenazas, gestionar claves, rotarlas, controlar accesos y decidir quién debe poder ver el texto en claro para empezar.
Checklist práctica de code review
Busca usos de Base64:
rg "btoa\\(|atob\\(|base64|Buffer\\.from\\(.*base64|toString\\('base64'\\)"
Clasifica cada resultado:
- Transporte: conversión de bytes a texto para JSON, MIME, data URI, PEM o almacenamiento. Suele ir bien.
- Formateo tras cripto: ciphertext, nonce, firma, salt o clave pública. Suele ir bien si la cripto es correcta.
- Interfaz opaca: cursor, código de invitación o estado que carga el cliente. Revisa autorización y protección contra manipulación.
- Ocultar secretos: clave de API, contraseña, token, credencial, claim privado o datos de usuario. No va bien.
- Nomenclatura engañosa: variables o textos de UI dicen cifrado, seguro, protegido, secreto, enmascarado u oculto. Arregla el nombre o la implementación.
Después pregunta:
- ¿Recibiría el valor codificado un navegador, una app móvil, un usuario, un colector de logs, un proxy o una herramienta de soporte?
- ¿Hay un decodificador al lado?
- ¿Un servidor verifica permisos tras decodificar?
- ¿El valor va firmado si el cliente puede editarlo?
- Si es un JWT, ¿se verifica la firma antes de cualquier decisión de confianza?
- Si es una contraseña, ¿por qué es reversible en absoluto?
Esto mantiene el review con los pies en la tierra. Un PNG en una data URI es aburrido. Una clave de API de producción en un bundle JavaScript no.
Respuesta a incidentes: si encuentras "secretos" en Base64
Si descubres un secreto codificado en Base64 en código fuente, logs, un ticket, analítica, una URL o un export de base de datos:
- Decodifícalo localmente para confirmar qué es.
- Trata el valor decodificado como expuesto.
- Rota credenciales, tokens y claves que hayan podido quedar a la vista.
- Elimina la copia codificada de código fuente, logs, capturas o tickets cuando puedas.
- Sustituye el diseño por un secreto en servidor, una clave pública con scope, estado firmado, cifrado real o hash de contraseña.
- Añade una regla de review para que el mismo patrón no vuelva.
No pierdas tiempo debatiendo si la cadena Base64 era "difícil de notar". Los escáneres automáticos, los usuarios del navegador y los atacantes pueden decodificarla igual.
Cuándo necesitas realmente un token cifrado
Un JWT firmado estándar usa JWS: payload legible, firma evidente a manipulación. Si de verdad necesitas confidencialidad de los claims, usa JWE (JSON Web Encryption).
Un JWE compacto tiene cinco secciones Base64url:
protected-header.encrypted-key.iv.ciphertext.authentication-tag
Esas secciones siguen siendo texto Base64url. La confidencialidad viene del algoritmo de cifrado JWE y de la clave del destinatario, no de Base64url en sí.
En muchas apps web, un diseño más simple funciona mejor: guarda el estado sensible en el servidor y manda al navegador un identificador de sesión corto y aleatorio. No todo JWT legible tiene que convertirse en JWE.
Preguntas frecuentes
¿Base64 es cifrado?
No. Base64 es codificación reversible sin clave secreta. Cualquiera con la cadena puede decodificarla con una función del navegador, un comando de terminal, una herramienta online o unas pocas líneas de código.
¿Al menos Base64 sirve como ofuscación?
Solo en el sentido más débil. Puede que oculte el valor de un vistazo casual, pero no detendrá a un desarrollador, un atacante, un escáner, una extensión de navegador, un lector de logs ni una herramienta de soporte.
¿Base64 es seguro para contraseñas o claves de API?
No. Las contraseñas necesitan hashing lento como Argon2id, bcrypt o scrypt. Las claves de API deben quedarse en el servidor o en un gestor de secretos con scopes, auditoría y rotación.
¿Cualquiera puede leer el payload de un JWT?
Sí, en los JWTs firmados corrientes. La cabecera y el payload son JSON codificado en Base64url. La firma puede probar la integridad tras verificarse, pero no oculta los claims.
¿Por qué Basic Auth usa Base64 si no es seguro?
Basic Auth usa Base64 para meter username:password en una cabecera HTTP. HTTPS/TLS aporta el cifrado en tránsito. Sin HTTPS, las credenciales de Basic Auth son básicamente texto plano en la red.
¿Base64url es más seguro que Base64?
No. Base64url sustituye + y / por - y _ y suele omitir el padding para que el valor funcione en URLs y JWTs. Sigue siendo codificación reversible.
¿Qué debería usar en vez de Base64 para cifrar?
Un diseño de cifrado autenticado revisado como AES-GCM, XChaCha20-Poly1305 o JWE, más generación de claves adecuada, almacenamiento de claves, rotación y control de acceso. No diseñes tu propio formato criptográfico.
¿Para qué sirve realmente Base64?
Base64 representa bytes como texto: campos binarios en JSON, adjuntos de correo, data URIs, ficheros PEM, firmas, formato de ciphertext y segmentos de token seguros para URL. Es un envoltorio, no una frontera de seguridad.
Codifica y decodifica en tu navegador
¿Necesitas inspeccionar una cadena Base64 ahora mismo? Base64 Encode & Decode en fixjson.org permite codificar o decodificar texto localmente en el navegador, incluyendo contenido UTF-8. Úsalo para inspeccionar payloads JWT, campos de respuestas de API o data: URIs, pero no confundas decodificar con verificar o descifrar.
Herramientas y guías relacionadas
- Base64 Encode & Decode: codifica y decodifica Base64 localmente en tu navegador.
- Decodifica cadenas Base64 y payloads JWT: decodificación práctica de Base64 y Base64url.
- Cómo decodificar un JWT: leer claims y entender por qué decodificar no es verificar.
- JSON sensible y herramientas locales: decidir qué es seguro pegar en herramientas de navegador.
- JSON Stringify: inspecciona y escapa cadenas JSON anidadas.
- RFC 4648: el estándar Base64: el trasfondo formal de Base64 y Base64url.
- RFC 7519: JSON Web Token: estructura del JWT y consideraciones de seguridad.
Fuentes
- RFC 4648: Base64, Base64url, padding, alfabetos y consideraciones de seguridad.
- RFC 7617: HTTP Basic Authentication y su formato de credenciales en Base64.
- RFC 7519: estructura del JWT, decisiones de confianza y consideraciones de privacidad.
- RFC 7516: JSON Web Encryption y la serialización compacta JWE.
- MDN btoa y MDN atob: primitivas Base64 del navegador y matices con UTF-8.
- OWASP Cryptographic Storage Cheat Sheet: cifrado, modos autenticados y gestión de claves.
- OWASP Password Storage Cheat Sheet: guía de hashing de contraseñas.
Última revisión julio de 2026.