[RFC] 8 min read

[RFC] Base64 URL-Safe vs Base64 estándar

Base64 estándar y Base64 URL-safe se ven casi idénticos pero rompen los decodificadores del otro. Aquí tienes exactamente qué cambia, por qué, y qué variante necesitas para JWT, OAuth y URLs.

April 2026 | standards

// TL;DR

Existen dos alfabetos Base64, ambos especificados en RFC 4648.

Base64 estándar (§4) — caracteres A–Z a–z 0–9 + /, con relleno =. Seguro para MIME, archivos PEM, HTTP Basic Auth, correo electrónico. Se rompe cuando se coloca en URLs, cookies, nombres de archivo o etiquetas DNS.

Base64 URL-safe, a veces escrito base64url (§5) — caracteres A–Z a–z 0–9 - _, el relleno generalmente se omite. Seguro para URLs, partes de JWT, tokens de estado OAuth, cookies, DNS, nombres de archivo.

Los decodificadores para un alfabeto rechazarán la salida del otro. Elige la variante que coincida con tu transporte y mantente en ella.

// LOS DOS ALFABETOS, LADO A LADO

// Position 62 and 63 are the only differences
//
// Standard (RFC 4648 §4)   URL-safe (RFC 4648 §5)
// 0–25   A-Z                A-Z
// 26–51  a-z                a-z
// 52–61  0-9                0-9
// 62     +                  -      (hyphen)
// 63     /                  _      (underscore)
// pad    =                  = or omitted

// Worked example: encoding 0xFB 0xFF 0xBF
// binary: 11111011 11111111 10111111
// 6-bit:  111110 111111 111110 111111
// values: 62     63     62     63
// Standard:  + / + /       → '+/+/'
// URL-safe:  - _ - _       → '-_-_'

// POR QUÉ NECESITAMOS UNA VARIANTE URL-SAFE

El alfabeto estándar fue diseñado para cuerpos MIME, que son permisivos. Las URLs no lo son. En RFC 3986, + a veces se decodifica como un espacio (heredado de los formularios HTML), / es un separador de rutas, y = es un sub-delimitador reservado. Si colocas una cadena Base64 estándar con los tres en una URL, diferentes analizadores no estarán de acuerdo sobre qué significa. La codificación por porcentaje lo arregla técnicamente (+%2B, /%2F, =%3D), pero el resultado es voluminoso y feo.

RFC 4648 §5 resuelve el problema eligiendo dos caracteres ASCII que no tienen significado especial en URLs: - (guion) y _ (guion bajo). Ambos no están reservados según RFC 3986 y sobreviven intactos a cada analizador de URL. El relleno = también es problemático en algunos contextos de URL (es el separador clave/valor en las cadenas de consulta), por lo que base64url convencionalmente lo descarta.

// CUÁNDO USAR CUÁL

  • > Base64 estándar — úsalo para:
  • > • Adjuntos de correo MIME (RFC 2045)
  • > • Certificados y claves en formato PEM (RFC 7468)
  • > • Encabezado HTTP Basic Auth (Authorization: Basic ...)
  • > • data: URIs (data:image/png;base64,...) — los caracteres +/= son todos legales dentro de atributos entre comillas
  • > • Firmas XML-DSIG y XML-ENC
  • > • S/MIME
  • > Base64 URL-safe — úsalo para:
  • > • Encabezados, cargas y firmas JWT (RFC 7515 obliga a base64url)
  • > • code_challenge / PKCE de OAuth 2.0 (RFC 7636)
  • > • Tokens state y nonce de OpenID Connect
  • > • Enlaces mágicos, códigos de invitación y tokens de URL cortos
  • > • Valores de cookies que deben sobrevivir al ser copiados en una URL
  • > • Nombres de archivo generados a partir de hashes (¡evita / en los nombres de archivo!)
  • > • Etiquetas DNS y registros TXT (el guion está permitido, la barra no)
  • > • Claves de almacenamiento direccionable por contenido (sistemas tipo IPFS)

// CONVERSIÓN ENTRE LOS DOS

Dado que los dos alfabetos solo difieren en las posiciones 62, 63 y el relleno, la conversión es un buscar-y-reemplazar de tres caracteres. Aquí está en cada runtime que probablemente uses.

// JavaScript (browser + Node)
const toUrlSafe = (b64) =>
  b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

const fromUrlSafe = (b64u) => {
  let s = b64u.replace(/-/g, '+').replace(/_/g, '/');
  while (s.length % 4) s += '=';   // re-add padding
  return s;
};

// Node 16+ native
const url = Buffer.from(data).toString('base64url');
const std = Buffer.from(url, 'base64url').toString('base64');

// Python
import base64
url = base64.urlsafe_b64encode(data).rstrip(b'=')      # no padding
std = base64.b64encode(data)

// Go
import "encoding/base64"
url := base64.RawURLEncoding.EncodeToString(data)  // no padding
std := base64.StdEncoding.EncodeToString(data)

// PHP
$url = rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
$std = base64_encode($data);

// Java
String url = Base64.getUrlEncoder().withoutPadding().encodeToString(data);
String std = Base64.getEncoder().encodeToString(data);

// Shell (GNU coreutils 9+)
echo -n 'hello' | base64 --wrap=0 | tr '+/' '-_' | tr -d '='

// LA CUESTIÓN DEL RELLENO

Base64 estándar siempre rellena la salida a un múltiplo de 4 caracteres con =. Base64 URL-safe generalmente omite el relleno (RFC 4648 §5 lo menciona explícitamente como opcional). JWT (RFC 7515) prohíbe el relleno por completo — eyJhbGciOiJIUzI1NiJ9 no tiene = al final. Los code challenges de OAuth (RFC 7636) también prohíben el relleno.

Los decodificadores deben manejar ambos. Un decodificador permisivo elimina cualquier = final y rellena internamente si la longitud de la carga no es múltiplo de 4. Un decodificador estricto rechaza cualquier cosa que no esté ya alineada a 4. Si estás escribiendo codificadores, coincide con la expectativa del receptor. Si estás escribiendo decodificadores, sé generoso.

¿Cuánto relleno agregar al decodificar un base64url sin relleno? La regla es: si la longitud de entrada módulo 4 es igual a 2, agrega dos =; si es igual a 3, agrega uno =; si es igual a 0, no agregues nada; si es igual a 1, la entrada es inválida (ningún base64 válido tiene esa longitud mod 4).

// Restore padding before decoding a JWT segment
function addPadding(b64u) {
  const mod = b64u.length % 4;
  if (mod === 2) return b64u + '==';
  if (mod === 3) return b64u + '=';
  if (mod === 0) return b64u;
  throw new Error('Invalid base64url length');
}

// QUÉ SE ROMPE SI LOS MEZCLAS

  • > Corrupción silenciosa de datos — un decodificador estándar alimentado con una carga URL-safe decodificará la mayoría de los caracteres correctamente, pero - y _ no están en su alfabeto. Dependiendo de la implementación, puede lanzar una excepción, puede tratarlos como basura (saltándolos) o puede producir silenciosamente bytes incorrectos.
  • > Desajustes de longitud — el decodificador ve una cadena cuya longitud no es múltiplo de 4 (porque se eliminó el relleno), y la rechaza directamente. Punto de dolor frecuente con bibliotecas JWT que esperan entrada con relleno.
  • > Sorpresas de codificación por porcentaje — si colocas accidentalmente Base64 estándar en una URL y un middleware codifica por porcentaje solo + y =, ahora tienes una carga con codificación mixta: válida para algunos decodificadores, basura para otros.
  • > Rechazos de cookies — varios servidores HTTP rechazan cookies que contienen / o = en el valor, aunque la especificación técnicamente lo permita. Base64 URL-safe evita esto por completo.
  • > Fallos de nombre de archivo en Windows — la salida estándar de Base64 contiene /, que es un separador de rutas. Nombrar un archivo con su SHA-256 en Base64 estándar falla tanto en Windows como en POSIX.
  • > Corrupción por copiar y pegar — algunos terminales y aplicaciones de chat hacen saltos de línea en /, rompiendo invisiblemente una cadena Base64 estándar a través de líneas. URL-safe no provoca esto.

// CASO DE ESTUDIO DEL MUNDO REAL: JWT

Un JSON Web Token se ve como header.payload.signature, tres cadenas base64url separadas por puntos. Cada biblioteca JWT importante (jose, jsonwebtoken, PyJWT, golang-jwt, java-jwt) codifica con base64url sin relleno, porque los tokens se colocan rutinariamente en URLs y encabezados HTTP.

Si decodificas un JWT a mano con atob() en el navegador, primero debes convertir: reemplaza - por +, _ por /, y agrega cualquier = faltante. Bibliotecas como jose manejan esto internamente.

// Decode a JWT payload in the browser
function decodeJwt(token) {
  const [, payloadB64u] = token.split('.');
  let b64 = payloadB64u.replace(/-/g, '+').replace(/_/g, '/');
  while (b64.length % 4) b64 += '=';
  const json = atob(b64);
  return JSON.parse(json);
}

// Example JWT payload
decodeJwt('eyJhbGciOiJIUzI1NiJ9' +
          '.eyJzdWIiOiJhYmMxMjMifQ' +
          '.sig').sub;     // 'abc123'

// DATA URIs: UN CASO LÍMITE

Los data URIs (data:image/png;base64,...) son técnicamente URLs, pero según RFC 2397 aceptan el alfabeto completo de Base64 estándar incluyendo + y /. Por eso ves Base64 estándar en los data URIs por todas partes — es la especificación.

Base64 URL-safe dentro de un data URI técnicamente viola RFC 2397 y puede ser rechazado por analizadores estrictos. Apégate a Base64 estándar para data URIs. El único momento en que URL-safe importa para los data URIs es si el propio URI se incrusta en otra URL como parámetro de consulta — en ese momento, deberías codificar por porcentaje toda la cosa en su lugar.

// BASE64URL NO ES CODIFICACIÓN DE URL

Vale la pena decirlo explícitamente, porque los nombres son confusamente similares. La codificación de URL (también llamada codificación por porcentaje, RFC 3986) transforma los caracteres reservados en secuencias %XX — es lo que le pasa a un espacio cuando lo escribes en la barra de direcciones del navegador (%20).

Base64 URL-safe es un sabor de Base64 diseñado para no necesitar codificación de URL. Produce una salida que ya es segura para URLs, por lo que no se requiere codificación por porcentaje. Nunca aplicas ambas en secuencia; aplicas una u otra dependiendo de tus datos.

Regla general: si los datos son binarios (una imagen, un hash, una firma) y necesitas colocarlos en una URL, usa base64url. Si los datos ya son texto (un valor de parámetro de consulta, un campo de formulario), usa codificación por porcentaje.

// ÁRBOL DE DECISIÓN

  • > ¿La cadena Base64 va a una URL, un valor de cookie, un nombre de archivo, una etiqueta DNS, un JWT o un flujo OAuth? → Base64 URL-safe.
  • > ¿Va a un cuerpo de correo electrónico, un archivo PEM, un encabezado HTTP Basic Auth, un data URI o un cuerpo multiparte MIME? → Base64 estándar.
  • > ¿El consumidor es una biblioteca que no controlas (analizador JWT, proveedor OAuth, cliente S/MIME)? → Coincide con lo que esperan; lee la especificación o los documentos de la biblioteca.
  • > ¿No estás seguro? → Usa URL-safe sin relleno. Es la variante más portátil; siempre puedes rellenarla para receptores estrictos.

// PRUÉBALO TÚ MISMO

Nuestro codificador Base64 produce Base64 estándar por defecto con un interruptor para salida URL-safe. Nuestro codificador base64url tiene como predeterminado URL-safe sin relleno. Úsalos lado a lado para ver exactamente qué caracteres cambian.

Lectura adicional:
¿Qué es Base64 y cómo funciona? — el agrupamiento de 6 bits que subyace a ambas variantes.
Base64 en JavaScript (navegador + Node) — helpers específicos por runtime.
Base64 vs Base64URL vs Base32 — cuándo ninguna de las variantes de Base64 es la elección correcta.