Transcript
00:00:00Sucedió de nuevo. Este es como mi tercer video sobre CVEs de Server Components este año y ni
00:00:05siquiera creo haberlos cubierto todos. Esta vez son 13 CVEs, sí 13, en React
00:00:11y Next.js, 6 de los cuales son de alta gravedad e incluyen denegación de servicio, bypasses
00:00:15de middleware, cross-site scripting y más. Quizás los Server Components fueron un error.
00:00:20Aquí está el lanzamiento de seguridad de Next.js, ya saben, arreglando unos cuantos problemas casuales
00:00:28que han tenido este mes y abajo, obviamente, la resolución es actualizar
00:00:32todas sus versiones de Next.js; estas son las versiones afectadas. Vale la pena notar
00:00:36que TanStack no está afectado por esto, lo cual, quizás sea parcial, pero es otra razón para usar
00:00:41TanStack para mí. No repasaré todos estos porque probablemente estaríamos aquí un buen rato
00:00:44y tampoco he encontrado exploits que funcionen para todos, pero quiero mostrarles uno
00:00:48de cada categoría y empezaremos con un bypass de middleware y proxy; el que
00:00:52logré recrear es este del Pages Router. Tenemos un bypass de proxy de middleware en el
00:00:56Pages Router si usas i18n y puedes ver que este es un CVE con una gravedad de
00:01:027.5 sobre 10. Este es un ejemplo de una aplicación vulnerable; en la configuración de Next.js he
00:01:06activado i18n y también configurado dos locales: inglés y francés; luego tengo un archivo de middleware
00:01:12que en versiones posteriores de Next.js se renombró a proxy para intentar evitar
00:01:16la confusión que estoy por mostrarles, pero esencialmente lo que el middleware debería permitirnos
00:01:19hacer es modificar una petición entrante, ya sea redirigiéndola, reescribiéndola o añadiendo
00:01:24algunos encabezados o algo así. En mi caso, lo uso para que si intentas visitar la
00:01:28página /secret, verifique si tienen una cookie de sesión para saber si están logueados y si
00:01:32no, debería redirigirlos a la página de login, así que ojalá solo usuarios autorizados vean
00:01:37mi página secreta. Al final también tenemos un “matcher” para que cualquier middleware que
00:01:41apliquemos a esa página secreta también coincida con las variantes de locale porque técnicamente,
00:01:45ahora que tenemos dos locales, tenemos tres versiones de esta URL. En la página secreta en sí,
00:01:50luego también tengo algunos server-side props; estos deberían obtenerse del servidor al renderizar
00:01:54y de nuevo, como configuramos ese middleware, teóricamente solo un usuario logueado debería ver
00:01:58cuáles son estos valores que luego uso en la página misma: un email, un flag y
00:02:03también un encabezado. De nuevo, solo un usuario autorizado debería ver esto. Pongámoslo
00:02:07a prueba entonces; lo primero que intentaré es acceder a esa página secreta y ven
00:02:11que soy redirigido al login porque no estoy logueado, lo que significa que el middleware funciona,
00:02:16pero ¿y si nos volvemos maestros hackers? Bueno, podríamos hacerlo primero inspeccionando elemento,
00:02:20cosas de hackers absolutamente locas, y luego en el script de next data aquí abajo debemos buscar
00:02:24nuestro build ID; en mi caso es este de aquí y podemos proceder a copiarlo.
00:02:28Luego debemos escribir una URL que es underscore next slash data slash el build ID que
00:02:32acabamos de copiar y luego la página a la que intentamos acceder punto JSON. Una vez hecho eso,
00:02:37pueden ver que recuperamos esos props que deberían haber estado tras ese middleware y protegidos,
00:02:40que en mi caso eran un flag, email, encabezado y también algo diciéndote que te suscribas
00:02:44para más noticias de desarrollo, tutoriales y trucos. Adelante, hazlo, espero haberte
00:02:48impresionado con mis locas habilidades de hacker, pero ¿por qué sucede esto en realidad? Bueno,
00:02:52esto es tan fácil de explicar como lo fue hacerlo; teníamos nuestra página secreta y server side props,
00:02:56en Next.js los server side props se sirven desde una URL que se ve así, pero nuestro middleware
00:03:00debería haber estado protegiendo esta ruta. El problema es que al usar i18n también
00:03:05teníamos otras dos URLs que eran la variante en inglés y francés; pueden ver que los
00:03:09server side props también obtienen las variantes en inglés y francés, y Next.js simplemente tenía código
00:03:13defectuoso que hacía que, si i18n estaba activado, no protegía el caso base; así que
00:03:18este no estaba incluido en el matcher, pero los otros dos sí; las versiones en inglés y francés estaban
00:03:22protegidas, pero no el caso base de slash secret. Podemos verlo rápidamente si cambio
00:03:26esta URL a la versión en inglés: sí soy redirigido a la página de login. Una vulnerabilidad increíblemente simple,
00:03:31pero seré honesto, estos bypasses de middleware suelen sonar mucho peor de lo que
00:03:35son; no son geniales, pero realmente no deberías proteger mucho solo con middleware de todos modos,
00:03:40y Next.js ni siquiera recomienda que lo hagas. Si tuvieras datos sensibles
00:03:44en esos server side props y no tuvieras ninguna lógica de auth en el servidor, bueno, siento que
00:03:48parte del problema es tuyo también; así que pasemos a uno más dañino: denegación de
00:03:53servicio. Hubo tres de estos, pero solo hubo uno que pude recrear de forma fiable,
00:03:56y fue este de aquí: denegación de servicio con Server Components; esto afecta a Next.js
00:04:01así como a cualquier cosa que use el paquete react-server-dom, que es básicamente solo Next.js
00:04:05y los otros frameworks que lo han copiado como Vinxt y algunos otros forks. TanStack
00:04:10Start no usa esto, así que no es vulnerable. Pueden ver que esto también tiene una gravedad
00:04:14de 7.5 sobre 10. Con este, todo lo que necesitas es una aplicación Next.js muy simple y,
00:04:18en ella, necesitas estar usando una Server Action, pero de nuevo, puede ser una muy simple. Este es
00:04:22el sitio funcionando y pueden ver que cuando refresco la página casi no hay
00:04:25carga, es casi instantáneo; para ponerle números reales, si envío esta petición,
00:04:29verán que se resuelve en 0.02 segundos, pero si ahora ejecuto mi exploit y luego
00:04:34envío esa petición de nuevo, esta vez tardó seis segundos y eso fue ejecutando el exploit
00:04:39una sola vez; imaginen qué pasaría si lo encadenara. Ahora, para entender este exploit debemos
00:04:42saber un poco sobre el protocolo React Flight, que es el formato que React usa
00:04:46para serializar árboles de componentes y datos entre el servidor y el cliente. Probablemente
00:04:50ya hayan visto esto antes; en esta página teníamos un formulario con una Server Action. Si voy
00:04:54a la pestaña de Red aquí y hago clic en enviar, verán que el payload realmente se
00:04:58envía como datos que se ven así, que parece algo sin sentido, y lo mismo para la
00:05:02respuesta aquí. Si copiamos este payload puedo explicar qué sucede cuando se
00:05:05envía al servidor. El primer paso es la deserialización e iniciará en el chunk 0 donde tenemos
00:05:10este $k1. Este $k1 es solo un puntero diciendo que habrá algunos datos de formulario aquí
00:05:16que comienzan con un guion bajo; así que tomará todas las otras claves que enviamos
00:05:20como parte de ese payload, pasará por todas ellas buscando un string que comience con
00:05:24un guion bajo y sabrá que esta será la clave y que este es el valor. Una vez
00:05:28hecho eso, podrá decir que tenemos nombre, email, mensaje y simplemente convertirá estos datos
00:05:32en este objeto que tenemos aquí abajo. Rápido y sencillo. El problema con este enfoque
00:05:36es qué sucede cuando lo escalamos. Digamos que añado otro puntero, esta vez buscando $k2,
00:05:41esto va a buscar todas las claves que comiencen con dos guiones bajos. El problema
00:05:44es que ahora, estando en $k1, va a pasar por estas seis buscando las
00:05:48que comiencen con un guion bajo y cuando pase a $k2 hará exactamente lo
00:05:52mismo pero buscando las que inicien con dos guiones bajos. Así que ahora pasamos por 12
00:05:56claves en total. Eso no está tan mal, pero llevémoslo al extremo. Si añadimos 199,999
00:06:03claves aleatorias al payload que estamos enviando y luego cambiamos nuestro array en el cero aquí
00:06:07para ir de $k1, $k2 hasta $k1000, significa que tendrá que buscar un guion bajo,
00:06:12dos guiones bajos, tres guiones bajos hasta mil guiones bajos por todas nuestras 200,000
00:06:17claves que tenemos en nuestro payload, y eso significa que en total hará 200 millones de
00:06:21comparaciones de strings. Como imaginarán, eso bloqueará el hilo por varios segundos.
00:06:25Este es el commit que creo solucionó el problema que teníamos. Pueden ver que
00:06:28pasan muchas cosas en este commit y, honestamente, es algo complejo, pero intentaré
00:06:33explicarlo lo mejor que pueda. Esencialmente, ahora usan un sistema basado en cursores para las claves,
00:06:36así que cargan todas estas 200,000 claves de nuestro payload en una lista y luego
00:06:41empezamos en el cero aquí buscando la referencia $k1 y comienza a bajar por
00:06:45esa lista con un cursor que no puede retroceder. Baja por aquí hasta $j1, ve que no
00:06:50coincide con el guion bajo necesario, pasa a $j2, tampoco coincide con un guion bajo
00:06:54y continúa por toda la lista de claves hasta $j199,999. Una vez que llega
00:07:01aquí, se da cuenta de que no hay coincidencia para $k1 y pasa a $k2. Ahora $k2 busca
00:07:06dos guiones bajos, pero el problema es que, al ser un sistema de cursor y este cursor
00:07:09no puede retroceder, se queda inmediatamente al final de la lista, así que eso también
00:07:14será indefinido y continuará así hasta $k1000. Así que esta vez solo hemos
00:07:18recorrido 200,000 claves. Básicamente, este fix ha reducido el número de operaciones de
00:07:23$k*n, donde $k es el número de referencias $k y $n es el número de claves, a
00:07:27solo $n+k. En nuestro caso pasamos de 200,000,000 de operaciones a 201,000, ya que
00:07:33todavía debe recorrer todas las claves y también esas referencias $k. Creo que este
00:07:37tweet de Prime resume muy bien la situación. Crear tu propio protocolo con serialización
00:07:41es increíblemente difícil, así que no sorprende que veamos tantos problemas. En mi opinión,
00:07:46deberían pedirle a Claude Mythos que le dé una revisada al código de React y Next.js. A
00:07:50continuación tenemos el CV de mayor gravedad de todos: Server-Side Request Forgery en
00:07:54Como pueden ver, esta tiene una puntuación de 8.6 sobre 10, pero también cabe
00:07:59notar que no afectó a implementaciones en Vercel, solo a las auto-alojadas u otros proveedores.
00:08:04Este exploit es también super simple de aprovechar. Primero lanzamos nuestro servidor Next.js
00:08:09y, de nuevo, puede ser una aplicación Next.js de base, sin modificaciones.
00:08:14Luego también querremos un servidor interno. Digamos que este servidor solo es accesible
00:08:18por el servidor Next.js y no por el mundo exterior, por ejemplo, en nuestro despliegue en la nube.
00:08:23Entonces lo que haremos es enviar una petición curl muy simple donde dirigimos
00:08:26el curl a nuestra aplicación Next.js en el puerto 3002 y decimos que nuestro
00:08:31“request target” sea el servidor al que queremos acceder en esa URL de localhost. Si ahora
00:08:36presiono enter, pueden ver lo que devuelve: es en realidad la página HTML de un servidor Python
00:08:40donde muestra el listado básico de directorios, y si vuelvo al servidor Python mismo,
00:08:45verán que tuvo una petición entrante que en realidad vino de la aplicación Next.js.
00:08:49Para visualizarlo mejor, digamos que en nuestra línea punteada está nuestro despliegue de Next.js,
00:08:53donde tenemos nuestro servidor Next.js y también algunos servicios internos, ya sea Redis,
00:08:57una base de datos o cualquier otro servicio, y este de aquí no quieres que tenga acceso público,
00:09:02así que nadie puede enviarle un curl; simplemente fallará, está tras un firewall y bloqueado,
00:09:06solo puede ser accedido por el servidor Next.js. Lo que hemos hecho es simplemente enviar
00:09:10un curl al servidor Next.js y decirle: “Oye, ¿puedes enviar una petición por nosotros
00:09:15al servicio interno?”, y obtuvimos la información de vuelta, así que saltamos el firewall
00:09:19pasando por Next.js, que sí tiene acceso al servicio interno. La causa raíz de
00:09:23esto es bastante simple: básicamente en nuestro curl enviamos un encabezado de
00:09:28“upgrade websocket” y cuando enviamos estos encabezados lo que sucede en Next.js es que llegamos a esta
00:09:32parte del código, y esto resuelve las rutas en nuestra URL, pero la URL procesada que obtenemos
00:09:37es en realidad el request target que enviamos en nuestro curl, así que este va a ser el
00:09:40objetivo al que intentamos llegar en ese servidor interno y no la aplicación Next.js en sí.
00:09:45Lo que pasa con esta URL procesada es que pasa por una verificación abajo para ver si
00:09:49tiene un protocolo, y en este caso sí, porque usamos HTTP y eso es un protocolo,
00:09:55así que procede a hacer el proxy de esa petición por nosotros. El fix para esto añade dos
00:09:58nuevas guardas en nuestra función resolve routes; ahora obtenemos un booleano de
00:10:02finalizado así como un código de estado. Lo que esta función realmente hace es
00:10:06tomar nuestra URL y procesar si es una petición de proxy legítima basada en nuestros
00:10:11rewrites, middleware y demás de Next.js; si no lo es, significa que “finished” será
00:10:15falso, así que abajo tenemos una comprobación: si es verdadero pasamos al siguiente paso, pero si
00:10:20no, no se ejecutará, lo que significa que no se hará la petición de proxy.
00:10:24En el caso de nuestra petición curl anterior, eso es exactamente lo que va a
00:10:27suceder, “finished” será falso. Ahora, si por alguna razón fuera verdadero, nuestra
00:10:32siguiente verificación es el código de estado; cuando resolve routes corre obtenemos un código si la
00:10:36petición es HTTP, será 200, 404 o lo que sea; así que si hay un código
00:10:41significa que no es una petición de proxy websocket válida, por lo que ignorará
00:10:45esta línea y no la ejecutará. He intentado profundizar en estos temas en este video,
00:10:49así que dime si sigues viendo comentando algo aleatorio, no sé, como
00:10:53“bar” o algo así, y suscríbete si aprecias el contenido. Nos quedan dos
00:10:58categorías más, pero estas deberían ser más rápidas, y empezaré
00:11:01con envenenamiento de caché, donde logré recrear este problema moderado de aquí.
00:11:04Es un envenenamiento de caché en React Server Component y tiene un ranking de 5.4 de 10. Para recrear
00:11:09esto tengo una aplicación Next.js pero también un CDN falso para simular que esto está
00:11:14en un entorno real desplegado. Esto significa que si visito mi sitio por primera vez en
00:11:18su URL de CDN, hago clic en ver productos, vuelvo y hago clic de nuevo, la primera vez
00:11:23debería ser un “cache miss” y la siguiente un “cache hit”. Podemos verlo en los logs
00:11:27aquí: primero tuvimos un miss en slash productos con un query string y las siguientes fueron un
00:11:31hit. Lo que hice luego fue limpiar la caché para simular que tal vez expiró
00:11:35o algo más, y ahora puedo enviar esta petición curl. De vuelta en mi aplicación,
00:11:39si hago clic en ver productos ahora, verán que recibimos un montón de basura; hemos
00:11:44hecho con éxito un ataque de envenenamiento de caché. Lo que creo que sucede es que cuando enviamos el
00:11:47curl con el encabezado de react-server-components en uno, esta URL devolverá sus datos de
00:11:52server component en lugar de HTML. Luego, cuando esto debe cachearse, Next pasará
00:11:58por una función que verifica si son datos de server component o no. Si lo son,
00:12:02los guardará en caché como tales, y si no, los guardará como HTML. Esto significa que,
00:12:07teóricamente, cuando el usuario intente obtener la versión HTML de la página, por ejemplo
00:12:11haciendo clic en el botón, nunca debería recibir datos de server component. El problema es que en
00:12:16nuestro caso, al tener ese query string al final de nuestra petición curl de server component,
00:12:20no cumplía con los requisitos para verificar si era un server component o no, ya que
00:12:24solo revisa si termina en .rsc, pero nuestra versión termina en el query string, así que
00:12:29ahora cree que es HTML y lo guarda en caché como tal; la próxima vez que un usuario hace
00:12:33clic en el botón, recibe los datos de server component porque el sistema creyó que
00:12:37era HTML. La solución para esto fue increíblemente simple: al verificar si
00:12:41termina en .rsc, ahora simplemente ignoran los query strings. Pasando ahora a nuestra
00:12:46categoría final de CVEs, tenemos algunos de cross site scripting; este es el que logré
00:12:50recrear: tiene un 6.1 de 10 y es un cross site scripting en scripts de tipo
00:12:55“before interactive” con entrada no confiable. Básicamente lo que esto significa es que en Next.js, si tengo
00:12:59una etiqueta script con estrategia de “before interactive”, y luego tengo otro atributo en ella que
00:13:03necesita contenido no confiable, por ejemplo, este viene de los parámetros de búsqueda,
00:13:08puedo hacer cross site scripting. Puedo hacerlo logrando que el usuario haga clic en un enlace
00:13:12como este, por ejemplo, donde he incrustado mucho contenido en ese parámetro de búsqueda; si alguien
00:13:16hiciera clic en ese link, esto es lo que vería; podrían pensar que tienen que
00:13:19loguearse de nuevo en el sitio, y cuando hacen clic en “sign in”, verán aquí que era un
00:13:22formulario de login totalmente falso inyectado vía ese parámetro; esencialmente permite al atacante
00:13:26ejecutar JavaScript en la máquina de la víctima; creo que un ejemplo más realista
00:13:31sería robar las cookies de sesión de Chrome para entrar en todo lo que tuvieras acceso.
00:13:34Este es un ejemplo simple de un escape incorrecto y podemos verlo más fácil con
00:13:39esta versión simplificada; todo lo que hago en ese parámetro es cerrar primero una
00:13:43etiqueta script y luego abrir una nueva con lo que yo quiera ejecutar; cuando presionamos
00:13:47enter, ven que sale la alerta que dice “pwned”. Como les mostré con mi app,
00:13:51la forma en que esto funciona es que necesitamos una etiqueta script de Next.js con estrategia “before interactive”
00:13:55y algún atributo en ella que tome datos de una fuente no
00:13:59confiable, en mi caso de los parámetros de consulta; lo que esto hace en Next.js es que esta
00:14:04etiqueta script se convierte en algo como esto, donde tenemos un “dangerouslySetInnerHTML”.
00:14:08El nombre ya indica que será peligroso, y también tenemos un JSON.stringify.
00:14:12Lo importante de JSON.stringify es que no escapa caracteres HTML
00:14:17como los corchetes de cierre; así que lo que realmente sucede aquí es que tomamos todos los scripts
00:14:21configurados en Next.js, busca la fuente que se establecerá aquí y
00:14:24luego el resto de los props contendrá el ID de seguimiento así como el valor que
00:14:29pusimos en nuestro parámetro de consulta, y eso entra en ese JSON stringify. Bueno, esto
00:14:33se renderiza en la página como algo así: tenemos nuestro ID de seguimiento,
00:14:37que era el resto de los props, pero también el string que insertamos vía parámetros
00:14:41de consulta; si expandimos cómo se ve realmente en la página, es algo como
00:14:45esto: tenemos nuestro script, el ID de seguimiento, pero justo después en realidad
00:14:49hay una etiqueta de cierre de script; así que termina el script que Next.js intentaba hacer y
00:14:53tras esto podemos ejecutar el script que queramos en la página; también tenemos este trozo extra
00:14:58al final porque, si no estuviera, los analytics se renderizarían en la
00:15:01página como texto ya que el HTML vería esto como texto; esto simplemente lo
00:15:05absorbe para que no haya señales claras de que algo malo pasa en la página. Y ahí lo tienen,
00:15:09esos son 13 CVEs para Next.js y React esta semana y cómo funcionaron algunos. Sinceramente,
00:15:14no sé qué pensar sobre esto; lo odio, y lo digo desde un lugar donde hace un
00:15:18par de años cada proyecto que hacía era en Next.js y pensaba que era lo mejor, el
00:15:22futuro; pero parece ser obstáculo tras obstáculo, da la sensación de que se han precipitado
00:15:26en algunas cosas y luego han tenido que arreglarlas. Personalmente, ahora estoy totalmente volcado con TanStack y
00:15:31también Astro cuando necesito un sitio basado en contenido; me parecen mucho más simples y,
00:15:35siendo honesto, también me gusta mucho lo que Cloudflare ha estado haciendo últimamente, así que estoy migrando
00:15:39mis proyectos allí; aunque todavía tengo unos 20 en Vercel y necesito ir
00:15:43a actualizarlos. ¿Qué piensan ustedes? ¿Serán útiles algún día los Server Components o hemos intentado y fallado?
00:15:48Díganmelo en los comentarios abajo, suscríbanse y, como siempre, nos vemos en
00:15:51el próximo.