El mismo fallo, corregido tres veces
Sin contraseña. Acceso de administrador de todos modos. Eso es lo que compra un token falsificado frente al verificador RS256 de fast-jwt: un fallo parcheado tres veces antes de que la corrección realmente aguantara. El 8 de octubre de 2026, la GitHub Advisory Database publicó CVE-2026-107722, el informe detrás de la corrección que por fin se sostuvo. [1] Lea el título despacio: «Incomplete patch of CVE-2026-34950», es decir, «corrección incompleta de CVE-2026-34950». Ahora lea el título de CVE-2026-34950, de abril: «Incomplete fix for CVE-2023-48223», «corrección incompleta de CVE-2023-48223». [3]
Tres avisos, una biblioteca, una misma clase de vulnerabilidad. Cada corrección cerraba exactamente la entrada que el investigador anterior había demostrado, y cada vez llegaba alguien con una entrada ligeramente distinta y pasaba de largo.
Esa cadena es la verdadera historia. La vulnerabilidad en sí es un ejemplo de manual de confusión de algoritmos, y si eso es todo lo que se lleva, actualizará a la 6.3.0 y seguirá adelante. Lo que merece su tiempo es por qué dos correcciones competentes fallaron y la tercera no, porque esa diferencia es algo que puede aplicar a sus propios parches el lunes mismo.
No he reproducido esto. Todo lo que sigue procede del aviso, del aviso anterior y del diff que corrigió ambos. [1][2][3]
CVE-2023-48223 the original
v
CVE-2026-34950 "incomplete fix" 9.1
v (added trim)
CVE-2026-107722 "incomplete patch" 9.8El servidor confía en la clave. El atacante controla la cadena de texto
La confusión de algoritmos de JWT, en una frase: el propio token le dice al servidor cómo verificarlo. Si una aplicación firma con RS256 y se puede convencer al verificador de usar HS256 en su lugar, la clave pública RSA deja de ser una clave pública y pasa a ser un secreto compartido. Es pública. El atacante la tiene. Firma un token con ella, el servidor calcula el mismo HMAC, y la firma coincide.
Lo interesante de fast-jwt es dónde se toma exactamente esa decisión. La biblioteca determina si su clave es asimétrica mirándola como una cadena de texto: si parece un bloque PEM, la trata como RSA; si no, la trata como un secreto HMAC en bruto. [1]
Es una decisión razonable, y casi todas las bibliotecas JWT hacen algo parecido. Pero también significa que la clasificación depende de una coincidencia de texto, y la coincidencia de texto tiene casos límite que la criptografía no tiene.
Por qué trim() parecía la corrección correcta
En abril, CVE-2026-34950 informó de que un espacio inicial burlaba la comprobación PEM. [3] El comparador era una expresión regular anclada al principio de la cadena, así que una clave que empezaba con un espacio en blanco no coincidía, se clasificaba como secreto en bruto, y se abría la vía HMAC.
La corrección en la 6.2.0 fue una llamada a trim() antes de la comprobación. [1][2] Mínima, obviamente correcta para la entrada reportada, publicada con una prueba de regresión. Apostaría a que la mayoría de los revisores la habría aprobado en menos de un minuto, y probablemente yo habría sido uno de ellos.
Pero la entrada reportada nunca fue el verdadero fallo. El fallo estaba en el anclaje. Una expresión regular que empieza con un acento circunflejo codifica una suposición: la cabecera PEM está en la posición cero. trim() no elimina esa suposición, simplemente la hace cierta para una categoría de caracteres, porque String.prototype.trim solo elimina lo que la especificación ECMAScript llama espacio en blanco. [1]
Todo lo demás sigue funcionando. Las pruebas de regresión que acompañaron a la corrección real enumeran lo que significa «todo lo demás» en la práctica: un byte NUL, un carácter de escape, DEL, caracteres de ancho cero, una línea de comentario, un envoltorio HTTP. [2] Cualquiera de ellos delante de la cabecera PEM, y la clave vuelve a ser un secreto en bruto.
El aviso lo expresa mejor de lo que yo podría: la corrección era textualmente estrecha, solo espacios en blanco, en lugar de abordar la clase completa. [1]
// 6.1.0 and earlier
/^-----BEGIN(?: (RSA))? PUBLIC KEY-----/
// 6.2.0: trim() first, same anchor
trimmedKey.match(publicKeyPemMatcher)
// a leading NUL still walks past itLa tercera corrección cambió la regla, no la entrada
Fíjese en qué hace realmente distinto la 6.3.0, porque no es una lista más larga de caracteres a eliminar. [2]
Deja de anclar. Un nuevo comparador encuentra una cabecera PEM en cualquier parte de la cadena, y la clave se recorta a partir de ahí. Una clave se trata como secreto HMAC en bruto solo cuando no contiene ninguna cabecera PEM en absoluto, y una vez que hay una cabecera presente, la clave ya nunca puede recaer en HMAC. La comprobación X.509 pasa de includes a startsWith. Una clave que contiene marcadores PEM pero no se clasifica con claridad ahora lanza un error, en lugar de caer silenciosamente en la rama HMAC.
El mensaje del commit formula la nueva regla en una sola línea, y es el tipo de frase que merece la pena robar para su propio código: una vez que hay una cabecera presente, debe clasificarse como material de clave asimétrica y nunca puede recaer en HMAC. [2]
Las dos primeras correcciones pusieron una entrada en una lista negra. La tercera puso una forma en una lista blanca. Esa es toda la diferencia, y por eso esta cierra la clase completa en lugar de una instancia más de ella.
// 6.3.0 - find it anywhere
/-----BEGIN [A-Z0-9 ]+?-----/
const { pem, isRawSecret } =
locatePem(trimmedKey)
if (isRawSecret) return hsAlgorithms
// unless algorithms explicitly pins HS256 - that still skips this checkFijar el algoritmo ayuda. Fijar el equivocado, no
Aquí está la parte en la que apostaría a que cae la gente. El aviso enumera las configuraciones vulnerables: sin lista blanca de algorithms, una devolución de llamada asíncrona para la clave, o algorithms fijado en HS256. La configuración que enumera como protegida es algorithms fijado en RS256. [1]
Fijar el algoritmo es el consejo estándar contra la confusión de algoritmos, y es un consejo correcto. Pero un equipo que hojea un aviso, ve HS256 en la descripción del ataque y fija HS256 ha dejado el verificador fijado en la elección del atacante. La lista blanca debe nombrar el algoritmo con el que realmente firma, no el que leyó en el análisis.
Cuando reviso código de autenticación, esta es la forma que busco primero: un control defensivo configurado a partir del informe de un incidente en lugar de a partir del propio diseño del sistema. Pasa la revisión porque visiblemente es un control, y apunta en la dirección equivocada.
¿Está su servicio realmente expuesto?
Más acotado de lo que sugiere un 9.8, y tienen que coincidir tres cosas. [1]
Primero, la versión resuelta de fast-jwt está entre la 6.2.0 y la 6.2.4 para este CVE en concreto; la 6.1.0 y anteriores tampoco están a salvo, simplemente están expuestas al segundo eslabón de la cadena, CVE-2026-34950. Compruebe qué instaló realmente npm, no lo que pide package.json, y compruebe la dependencia transitiva: fast-jwt es el backend JWT por defecto de @fastify/jwt, así que muchos equipos lo están usando sin haberlo elegido. [1]
Segundo, el verificador está configurado de alguna de las formas vulnerables anteriores. Tercero, y esto es lo único que nadie puede comprobar por usted: su clave configurada lleva un carácter que no es espacio en blanco antes de su cabecera PEM. Una clave limpia no se ve afectada en absoluto.
Esa última condición es una propiedad de cómo la clave llega al proceso, no de su código. Leída de un archivo, pegada en una variable de entorno, extraída de un gestor de secretos, concatenada con una etiqueta por un script de despliegue. No tengo datos sobre la frecuencia con la que esto ocurre en la práctica, y el aviso tampoco, así que trátelo como un sitio donde mirar, no como una probabilidad.
Qué comprobar hoy
Tres cosas, en el orden en que yo las haría.
Actualice a la 6.3.0 y confirme después la versión resuelta. Luego establezca una lista blanca explícita de algorithms que nombre el algoritmo asimétrico que su servicio usa realmente. Y después, esta es la que sobrevive a este CVE concreto, verifique la forma de su clave de verificación en el arranque en lugar de confiar en lo que le entregue el archivo o la variable de entorno.
Esa tercera comprobación cuesta una línea y habría dejado inexplotables en su servicio los dos últimos de estos CVE, incluido el siguiente. (El primero, CVE-2023-48223, tenía otra forma: una clave válida que el comparador antiguo no reconocía, no un prefijo basura delante de ella, así que esta comprobación concreta no lo habría detectado.)
Esa es la lección real. Es probable que una biblioteca que ha parcheado tres veces la misma clase vuelva a hacerlo. A un cargador de claves que rechaza cualquier cosa que no sea exactamente un bloque PEM, eso le da igual.
# the version that actually loads
npm ls fast-jwt
# pin the algorithm you really use
{ algorithms: ['RS256'] }
# assert the key's shape at boot
key.startsWith('-----BEGIN')Corrija la regla, no el informe
Cada una de estas correcciones la hizo alguien competente, respondiendo a un informe real, con una prueba. El fallo no fue falta de cuidado. Fue de alcance: el informe describía una entrada, así que la corrección abordó la entrada, y la regla que hacía peligrosa esa entrada se quedó exactamente igual.
La pregunta que hago al revisar un parche es deliberadamente incómoda: ¿de qué clase de entrada es un ejemplo este informe, y cierra la corrección esa clase? Si la respuesta es no, no ha prevenido el próximo CVE, le ha puesto fecha.
Esa pregunta es la mayor parte de lo que es una revisión de seguridad del código. Rastrear un informe hasta la decisión que lo hizo posible, y después comprobar si la corrección movió esa decisión o solo el síntoma, es un trabajo que hago habitualmente en código de clientes, y es precisamente el tipo de cosa que un escáner nunca marcará, porque el número de versión parcheada parece correcto. Si quiere que alguien mire un parche con ojos de atacante antes de que otro encuentre el hueco, esa es la conversación que conviene empezar.
Fuentes
- [1] GitHub Advisory Database, GHSA-ww5h-9m49-7xx4 (CVE-2026-107722), fast-jwt: corrección incompleta de CVE-2026-34950
- [2] nearform/fast-jwt, commit d96bbc6: rechazo de prefijos de clave que no son espacio en blanco para cerrar la confusión de RSA a HS256
- [3] GitHub Advisory Database, GHSA-mvf2-f6gm-w987 (CVE-2026-34950), fast-jwt: corrección incompleta de CVE-2023-48223
¿Necesita que se compruebe esto en su propio producto?
Revisión de seguridad del código¿Quiere aprender a encontrar vulnerabilidades como esta por su cuenta?
Curso individual de revisión de código seguro