Los JSON Web Tokens (JWT) se utilizan habitualmente para implementar mecanismos de autenticación y autorización en aplicaciones web y APIs.
Un error aparentemente pequeño en la implementación puede convertir un JWT en un mecanismo de autenticación completamente inseguro.
Uno de los problemas más graves aparece cuando una aplicación decodifica el contenido del JWT pero no valida su firma.
En ese escenario, un atacante puede modificar el contenido del token y establecer, por ejemplo:
{
"upn": "admin"
}
Si la aplicación confía directamente en ese valor, puede terminar concediendo privilegios de administrador.
¿Qué es un JWT?
Un JWT tiene normalmente tres partes:
HEADER.PAYLOAD.SIGNATURE
Por ejemplo:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1cG4iOiJ1c3VhcmlvIn0.
firma
El header indica información sobre el token:
{
"alg": "HS256",
"typ": "JWT"
}
El payload contiene las afirmaciones o claims:
{
"upn": "usuario"
}
Y la signature permite comprobar que el contenido del token no ha sido modificado y que ha sido generado utilizando la clave correspondiente.
El problema: confiar únicamente en el payload
Imaginemos una aplicación Python que recibe un JWT y realiza la siguiente comprobación:
import jwt
def autenticar(token):
# ❌ Vulnerable:
# se decodifica el token sin verificar la firma
datos = jwt.decode(
token,
options={"verify_signature": False}
)
if datos.get("upn") == "admin":
return "Acceso concedido como administrador"
return "Acceso denegado"
El problema está aquí:
verify_signature=False
La aplicación está diciendo:
«No necesito comprobar quién ha firmado este token. Voy a confiar directamente en su contenido».
Y eso es precisamente lo que no debe hacerse.
¿Qué puede hacer un atacante?
Si el atacante consigue modificar el JWT, puede cambiar:
{
"upn": "usuario"
}
por:
{
"upn": "admin"
}
La aplicación vulnerable únicamente comprobará:
if datos.get("upn") == "admin":
Y el resultado será:
Acceso concedido como administrador
No se ha demostrado que el usuario sea realmente admin.
Simplemente se ha leído una cadena de texto dentro del token.
Un ejemplo conceptual
El atacante podría intentar construir un token con un encabezado como:
{
"alg": "none",
"typ": "JWT"
}
y un payload:
{
"upn": "admin"
}
La idea sería:
HEADER
{
"alg": "none",
"typ": "JWT"
}
+
PAYLOAD
{
"upn": "admin"
}
+
SIN FIRMA
El problema no es solamente la posibilidad de utilizar alg: none.
El problema fundamental es que la aplicación no está verificando criptográficamente la autenticidad del token.
¿Cómo debe hacerse correctamente?
La aplicación debe validar la firma utilizando la clave esperada y el algoritmo permitido.
Por ejemplo, utilizando Python y PyJWT:
import jwt
SECRET_KEY = "mi-clave-secreta"
def autenticar(token):
try:
datos = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"]
)
if datos.get("upn") == "admin":
return "Acceso concedido como administrador"
return "Acceso denegado"
except jwt.InvalidTokenError:
return "Token inválido"
Ahora la situación cambia.
Si alguien modifica:
{
"upn": "usuario"
}
para convertirlo en:
{
"upn": "admin"
}
la firma original deja de corresponder con el contenido del token.
Por tanto, la aplicación rechazará el JWT.
La diferencia fundamental
Implementación vulnerable
JWT
│
▼
Decodificar
│
▼
Leer "upn"
│
├── admin → ACCESO
│
└── otro → DENEGADO
La aplicación confía en el contenido.
Implementación correcta
JWT
│
▼
Validar firma
│
├── ❌ inválida → RECHAZAR
│
└── ✅ válida
│
▼
Validar claims
│
▼
Comprobar permisos
Aquí el contenido del JWT solamente se utiliza después de haber comprobado su autenticidad e integridad.
No basta con comprobar upn
Otro error habitual es asumir que un JWT válido implica automáticamente que el usuario puede realizar cualquier operación.
La autenticación y la autorización son conceptos diferentes.
Por ejemplo:
datos = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"]
)
usuario = datos.get("upn")
rol = datos.get("role")
Después deberían comprobarse los permisos necesarios para la operación concreta:
if rol != "admin":
return "Acceso denegado"
La aplicación no debería convertir simplemente:
upn = admin
en:
usuario administrador
sin una política de autorización adecuada.
También hay que validar otros claims
Además de la firma, un JWT puede contener información como:
{
"upn": "usuario",
"iss": "https://ejemplo.com",
"aud": "mi-api",
"exp": 1790900000,
"iat": 1790896400
}
Entre los elementos que pueden ser relevantes están:
iss: emisor del token.aud: audiencia prevista.exp: fecha de expiración.iat: momento de emisión.nbf: momento a partir del cual el token es válido.upn: identidad del usuario.role: rol o información utilizada para autorización.
Por ejemplo:
datos = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"],
audience="mi-api",
issuer="https://ejemplo.com"
)
De esta forma no se comprueba únicamente la firma, sino también determinados atributos esperados del token.
Una regla sencilla
Una aplicación no debería pensar:
"El JWT dice que soy admin,
por tanto soy admin."
Debe pensar:
"El JWT tiene una firma válida,
procede del emisor esperado,
está destinado a esta aplicación,
no ha expirado
y sus claims permiten esta operación."
Conclusión
Un JWT no es una fuente de verdad simplemente porque tenga formato JWT.
El payload puede contener:
{
"upn": "admin"
}
pero eso no demuestra que el usuario sea administrador.
La seguridad depende de que la aplicación valide correctamente la firma y las condiciones de validez del token antes de confiar en sus claims.
La vulnerabilidad se puede resumir en una línea:
No validar la firma del JWT = confiar en datos controlados por el atacante.
Y si la aplicación utiliza esos datos para decidir quién es administrador, una simple modificación de:
"upn": "usuario"
a:
"upn": "admin"
puede convertirse en una escalada de privilegios.
Un JWT firmado no es seguro por sí mismo. Es la aplicación la que debe verificar que esa firma es válida.