Dev.to Security 🔐 Cybersecurity 👁 0 📖 3 min read

El parche obvio del JWT no cierra la forja: 2 forjas aceptadas, luego 1, nunca 0

La confusión de algoritmo en JWT tiene fama de problema resuelto. Está en todas las listas: alg: none, RS256 → HS256, «valida el algoritmo». Y como está resuelto en la literatura, se resuelve mal en el código: se añade u

La confusión de algoritmo en JWT tiene fama de problema resuelto. Está en todas las
listas: alg: none, RS256 → HS256, «valida el algoritmo». Y como está resuelto en la
literatura, se resuelve mal en el código: se añade un if que rechaza none, se da el
caso por cerrado y el tablero se pone verde.

Escribí tres verificadores del mismo servicio y medí qué pasa con el mismo ataque contra
cada uno. No con opinión: con código de salida.

El servicio

Un validador de tokens que firma con RSA y publica su clave pública —como cualquier
despliegue que expone un JWKS—. El token declara sub, role y exp. El ataque forja
{"sub": "alice", "role": "admin"}: escalada sobre la cuenta de otra persona, sin
credenciales y sin acceso a la red interna.

Tres corridas, un comando

python3 exploit_poc.py                                  # servicio vulnerable
python3 exploit_poc.py --app patch_secure.py --naive    # el parche obvio
python3 exploit_poc.py --app patch_secure.py            # el parche canónico
sujeto forjas aceptadas código de salida
servicio vulnerable 2 de 2 0
parche obvio (alg=none cerrado) 1 de 2 0
parche canónico 0 de 2 1

El número del medio es el que importa. La condición obvia —rechazar los tokens sin
firma— se cumple y el ataque sigue entrando, porque el ataque interesante no era ése.

Por qué el parche obvio no basta

Son dos decisiones independientes, inocentes cada una por separado:

  1. El algoritmo lo elige el token. La cabecera no está firmada antes de decidir con qué verificarla; es el huevo y la gallina. Un verificador no puede preguntarle al atacante cómo quiere que lo verifiquen.
  2. Un solo material de clave para dos mundos. RSA necesita una clave pública; HMAC necesita un secreto compartido. Si el código carga «la clave» una vez y la pasa a las dos ramas, la rama HMAC termina usando algo que todo el mundo conoce.

Cerrar alg=none toca el primero, y solo para el caso trivial. El material compartido
sigue ahí, la rama HMAC sigue ahí, y el atacante solo tiene que declarar HS256 y firmar
con la clave pública que el servicio publica.

El parche que sí

El algoritmo deja de venir del token:

ALGORITMOS_PERMITIDOS = frozenset({"RS256"})

alg = cabecera.get("alg")
if alg not in ALGORITMOS_PERMITIDOS:
    raise InvalidToken(f"algoritmo fuera de la lista blanca: {alg!r}")

No es una lista negra más larga: es la desaparición de la rama. Sin rama HMAC, la
clave pública no puede convertirse en secreto de nadie. La lista blanca no añade una
comprobación; elimina un camino.

El control que hace honesta la medición

Las tres corridas empiezan verificando que el token legítimo entra en el servicio. Y en
las tres entra. Eso es lo que separa «bloqueé el ataque» de «rompí todo»: un parche que
deja el servicio inservible también rechaza el PoC, y devuelve el mismo código de salida.
Un test que solo comprueba que el login funciona no distingue una cosa de la otra.

Reproducirlo

Cinco segundos, sin dependencias, sin red, sin estado entre corridas:

git clone --depth 1 https://github.com/alexisromtz-lab/offensive-reproducers
cd offensive-reproducers/001-jwt-algorithm-confusion
python3 exploit_poc.py --app patch_secure.py --naive   # RC 0 · sigue entrando
python3 exploit_poc.py --app patch_secure.py           # RC 1 · bloqueado

Cada cápsula del repositorio sigue el mismo molde de tres veredictos —vulnerable, parche
obvio, parche canónico— con el código de salida como veredicto, para que la diferencia se
pueda medir en vez de creer.

Si auditas esto mañana

Cuando alguien diga «validamos el algoritmo», las tres preguntas son: ¿la lista es
blanca o negra?, ¿existe todavía la rama del otro algoritmo?, ¿el material de clave es
distinto en cada rama? Con una respuesta negativa, el alg=none cerrado no significa
nada.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.