C’est fini pour NextJS… 13 NOUVELLES vulnérabilités

BBetter Stack
Computing/SoftwareBusiness NewsInternet Technology

Transcript

00:00:00C'est encore arrivé. C'est ma troisième vidéo sur les CVE des Server Components cette année et je
00:00:05ne pense même pas les avoir toutes couvertes. Cette fois, il y en a 13, oui 13 CVE, sur React
00:00:11et Next.js, dont 6 sont de sévérité élevée, incluant des dénis de service, des contournements
00:00:15de middleware, du XSS et plus encore. Peut-être que les Server Components étaient une erreur.
00:00:20Voici donc la note de sécurité de Next.js, corrigeant juste quelques problèmes "ordinaires"
00:00:28survenus ce mois-ci, et tout en bas, la résolution est évidemment de mettre à jour
00:00:32toutes vos versions de Next.js. Voici les versions impactées. Il est à noter
00:00:36que TanStack n'est pas impacté, ce qui me donne une autre raison d'utiliser
00:00:41TanStack, même si je ne suis pas objectif. Je ne vais pas toutes les passer en revue car
00:00:44nous y passerions du temps et je n'ai pas trouvé d'exploits fonctionnels pour chacune,
00:00:48mais je veux vous en montrer une par catégorie, en commençant par le contournement
00:00:52de middleware et de proxy. Celle que j'ai recréée concerne le Pages Router.
00:00:56On a un contournement dans le Pages Router avec i18n, une CVE avec un score de
00:01:027.5 sur 10. Voici un exemple d'application vulnérable : dans la config Next.js, j'ai
00:01:06activé i18n avec deux locales, anglais et français. J'ai aussi un fichier middleware,
00:01:12renommé "proxy" dans les versions récentes pour éviter la confusion que je vais
00:01:16vous montrer, mais en gros, le middleware doit nous permettre de modifier
00:01:19une requête entrante, que ce soit par redirection, réécriture ou ajout de headers.
00:01:24Ici, je l'utilise pour la page "/secret" : il vérifie s'il y a un cookie de session,
00:01:28donc si l'utilisateur est connecté. Sinon, il doit le rediriger vers la page de login
00:01:32pour que seuls les utilisateurs authentifiés voient ma page secrète.
00:01:37En bas, on a un "matcher" pour que le middleware appliqué à cette page secrète
00:01:41corresponde aussi aux variantes de langues car techniquement, avec deux locales,
00:01:45on a trois versions de cette URL. Dans la page secrète elle-même,
00:01:50j'ai des "server-side props" récupérées sur le serveur au moment du rendu.
00:01:54Théoriquement, grâce au middleware, seul un utilisateur connecté devrait voir
00:01:58ces valeurs utilisées sur la page : un email, un flag et un titre.
00:02:03Seul un utilisateur autorisé doit y accéder. Mettons cela à l'épreuve.
00:02:07D'abord, j'essaie d'accéder à la page secrète : je suis redirigé vers le login,
00:02:11donc le middleware fonctionne. Mais si on devenait des hackers experts ?
00:02:16On peut faire ça en inspectant l'élément, truc de hacker de fou,
00:02:20puis dans le script NEXT_DATA ici, on cherche notre ID de build,
00:02:24celui-ci dans mon cas, et on le copie.
00:02:28Ensuite, on tape une URL : _next/data/ l'ID de build
00:02:32puis la page ciblée .json. Une fois fait,
00:02:37on récupère les props qui auraient dû être protégées par le middleware,
00:02:40ici le flag, l'email, le titre et un message vous invitant à vous abonner
00:02:44pour plus d'actus dév, tutoriels et astuces. Faites-le donc, j'espère
00:02:48vous avoir impressionnés par mes talents de hacker. Pourquoi cela arrive-t-il ?
00:02:52C'est aussi simple à expliquer qu'à faire. Avec notre page secrète et ses props,
00:02:56les props server-side sont servies via une URL comme celle-ci, et le middleware
00:03:00devait protéger cette route. Le souci est qu'avec i18n activé, on avait
00:03:05deux autres URLs : les variantes anglaise et française. Les server-side props
00:03:09ont aussi ces variantes, et Next.js avait du code défectueux qui faisait que
00:03:13si i18n était activé, le cas de base n'était pas protégé par le matcher,
00:03:18contrairement aux versions anglaise et française. Le chemin "/secret" pur
00:03:22était exposé. On le voit vite si je change l'URL pour la version anglaise :
00:03:26je suis bien redirigé vers le login. Vulnérabilité incroyablement simple,
00:03:31mais pour être honnête, ces contournements de middleware semblent souvent pires
00:03:35qu'ils ne le sont. Ce n'est pas idéal, mais on ne devrait rien protéger uniquement
00:03:40avec du middleware de toute façon, et Next.js ne le recommande pas.
00:03:44Si vous aviez des données sensibles dans ces props sans aucune logique d'auth
00:03:48côté serveur, le tort est un peu partagé. Passons à plus grave : le déni
00:03:53de service. Il y en avait trois, mais je n'en ai recréé qu'un seul de façon fiable,
00:03:56celui lié aux Server Components. Cela impacte Next.js ainsi que tout ce qui
00:04:01utilise le package react-server-dom, soit Next.js et ses dérivés comme Vinxi.
00:04:05TanStack Start ne l'utilise pas, donc n'est pas vulnérable. C'est aussi
00:04:10une sévérité de 7.5 sur 10. Il suffit d'une application Next.js simple
00:04:14utilisant une Server Action, même très basique. Voici le site qui tourne.
00:04:18Quand je rafraîchis, c'est presque instantané.
00:04:22Pour avoir des chiffres, si j'envoie cette requête,
00:04:25elle se résout en 0,02 seconde. Mais si je lance mon exploit
00:04:29puis renvoie la requête, elle prend maintenant six secondes.
00:04:34Et ce n'est qu'avec un seul exploit, imaginez si je les enchaîne.
00:04:39Pour comprendre l'exploit, il faut connaître le protocole React Flight,
00:04:42le format utilisé pour sérialiser les composants et données entre serveur
00:04:46et client. Vous l'avez sûrement déjà vu : sur cette page, on a un formulaire
00:04:50avec une Server Action. Dans l'onglet réseau, quand je clique sur envoyer,
00:04:54le payload envoyé ressemble à du charabia,
00:04:58tout comme la réponse ici. En copiant ce payload,
00:05:02je peux expliquer ce qu'il se passe côté serveur.
00:05:05D'abord la désérialisation, qui commence sur le chunk 0 avec ce $k1.
00:05:10Ce $k1 est un pointeur indiquant des données de formulaire commençant
00:05:16par un underscore. Il va parcourir toutes les clés envoyées dans le payload
00:05:20en cherchant une chaîne qui commence par un seul underscore pour identifier
00:05:24la clé et sa valeur. Une fois fait, il reconnaît nom, email, message,
00:05:28et transforme ces données en l'objet que vous voyez ici. Simple.
00:05:32Le problème arrive quand on passe à l'échelle. Disons que j'ajoute
00:05:36un autre pointeur $k2 cherchant les clés avec deux underscores.
00:05:41Désormais, pour $k1, il parcourt les six clés pour trouver celles
00:05:44avec un underscore, et pour $k2, il fait la même chose
00:05:48en cherchant les deux underscores. On est à 12 comparaisons au total.
00:05:52Rien de grave, mais poussons à l'extrême. Si on ajoute 199 999 clés
00:05:56aléatoires au payload et qu'on modifie notre tableau pour aller de
00:06:03$k1 à $k1000, il devra chercher de un à mille underscores
00:06:07parmi nos 200 000 clés du payload.
00:06:12Cela représente 200 millions de comparaisons de chaînes.
00:06:17Comme vous l'imaginez, cela va bloquer le thread pendant plusieurs secondes.
00:06:21Voici le commit qui a corrigé le problème selon moi.
00:06:25C'est assez complexe, mais je vais essayer d'expliquer au mieux.
00:06:28En gros, ils utilisent maintenant un système de curseur pour les clés :
00:06:33ils chargent les 200 000 clés du payload dans une liste, et on commence
00:06:36par chercher la référence $k1 en descendant la liste avec un curseur
00:06:41qui ne peut pas revenir en arrière. Il vérifie $j1, ça ne correspond pas
00:06:45à l'underscore voulu, alors il passe à $j2, et ainsi de suite
00:06:50jusqu'au bout de la liste, $j199 999.
00:06:54Arrivé là, s'il n'y a pas de match pour $k1, il passe à $k2.
00:07:01Mais comme le curseur ne peut pas reculer, il atteint immédiatement
00:07:06la fin de la liste. Ce sera donc indéfini, et ainsi de suite jusqu'à $k1000.
00:07:09Au final, on n'a parcouru que les 200 000 clés une seule fois.
00:07:14Le correctif a réduit le nombre d'opérations de k*n (k références
00:07:18pour n clés) à n+k. On est passés de 200 millions à 201 000 opérations,
00:07:23car il faut quand même parcourir les clés et les références $k.
00:07:27Ce tweet de Prime résume bien la situation : créer son propre protocole
00:07:33avec sérialisation est extrêmement difficile, pas étonnant qu'il y ait des failles.
00:07:37À mon avis, ils devraient demander à Claude Mythos de relire le code
00:07:41de React et Next.js. Ensuite, on a la CVE la plus sévère du lot :
00:07:46du SSRF (Server-Side Request Forgery) dans les applications Next.js.
00:07:50Celle-ci est notée 8.6 sur 10, mais ne concerne pas les déploiements
00:07:54hébergés sur Vercel, seulement l'auto-hébergement ou d'autres fournisseurs.
00:07:59L'exploit est très simple. On lance notre serveur Next.js,
00:08:04même une application par défaut sans aucune modification.
00:08:09Imaginons aussi un serveur interne, accessible uniquement par le serveur
00:08:14Next.js et pas par le monde extérieur, sur notre cloud par exemple.
00:08:18On envoie alors une simple requête curl à notre application Next.js
00:08:23sur le port 3002, en indiquant que notre cible de requête
00:08:26est l'URL localhost du serveur interne auquel on veut accéder.
00:08:31En validant, regardez ce qui revient : c'est la page HTML d'un serveur
00:08:36Python avec la liste des fichiers, et sur le serveur Python lui-même,
00:08:40on voit une requête entrante provenant de l'application Next.js.
00:08:45Pour mieux visualiser : dans notre périmètre de déploiement, on a notre
00:08:49serveur Next.js et des services internes (Redis, base de données, etc.).
00:08:53Ces services ne doivent pas être publics : une requête curl externe
00:08:57échouerait car ils sont derrière un pare-feu, accessibles seulement par Next.js.
00:09:02Ce qu'on a fait, c'est envoyer une requête à Next.js en lui disant :
00:09:06"Hé, peux-tu envoyer une requête pour moi à ce service interne ?"
00:09:10On a ainsi contourné le pare-feu en passant par Next.js.
00:09:15Nous avons obtenu l'information en contournant le pare-feu via Next.js
00:09:19qui, lui, a accès au service interne. La cause racine est assez simple :
00:09:23dans la requête curl, on envoie un header "upgrade websocket".
00:09:28Avec ces headers, Next.js atteint ce morceau de code qui résout les routes
00:09:32sur notre URL, mais l'URL analysée ici est en fait la cible de la requête
00:09:37envoyée dans notre curl, donc le serveur interne, et non l'application
00:09:40Next.js. Cette URL passe ensuite un test pour vérifier si elle a un protocole.
00:09:45Comme on utilise HTTP, c'est oui, et Next.js proxie donc la requête.
00:09:49Le correctif ajoute deux gardes à la fonction resolveRoutes : un booléen
00:09:55"finished" et un code de statut. resolveRoutes traite l'URL pour voir
00:09:58si c'est une requête proxy légitime basée sur les rewrites ou middlewares.
00:10:02Sinon, "finished" passe à faux. En bas, on vérifie : si c'est vrai,
00:10:06on continue, sinon le proxy ne s'exécute pas. Pour notre curl,
00:10:11"finished" sera bien à faux. Si jamais c'était à vrai, le second check
00:10:15est le code de statut. Si c'est une requête HTTP (200, 404...), ce n'est pas
00:10:20une requête proxy WebSocket valide, donc elle est ignorée. J'ai vraiment
00:10:24essayé de creuser ces sujets. Dites-moi si vous êtes encore là en
00:10:27mettant un commentaire au hasard, genre "bar" ou autre, et abonnez-vous.
00:10:32Il reste deux catégories plus rapides. D'abord l'empoisonnement de cache,
00:10:36avec ce problème modéré dans les React Server Components, noté 5.4 sur 10.
00:10:41Pour le recréer, j'ai une application Next.js et un faux CDN pour simuler
00:10:45un environnement réel. En visitant le site via le CDN et en naviguant
00:10:49dans les produits, la première fois est un "cache miss", puis un "cache hit".
00:10:53On le voit dans les logs : d'abord un miss sur /products avec query string,
00:10:58puis des hits. Ensuite, j'ai vidé le cache pour simuler une expiration,
00:11:01puis j'envoie cette requête curl. Sur l'application, si je clique
00:11:04sur voir les produits, je reçois plein de charabia : l'attaque a réussi.
00:11:09Il semble que si on envoie un curl avec le header react-server-components,
00:11:14l'URL renvoie des données de composant au lieu de l'HTML. Next.js vérifie
00:11:18alors s'il doit stocker cela comme du Server Component ou de l'HTML.
00:11:23Théoriquement, un utilisateur demandant la page HTML ne devrait jamais
00:11:27recevoir de données RSC. Mais avec notre query string à la fin de la requête
00:11:31curl, le test (qui vérifie si l'URL finit par .rsc) échouait.
00:11:35Next.js pensait donc que c'était de l'HTML et le stockait comme tel.
00:11:39Résultat : l'utilisateur suivant recevait les données RSC brutes.
00:11:44Le correctif est très simple : ils ignorent désormais les query strings
00:11:47lors du check de l'extension .rsc. Enfin, la dernière catégorie :
00:11:52le Cross-Site Scripting (XSS). En voici une à 6.1 sur 10 dans les scripts
00:11:58"before interactive" avec une entrée non fiable. En gros, dans Next.js,
00:12:02si un tag script a la stratégie "beforeInteractive" et un attribut
00:12:07provenant d'un contenu externe (comme des search params), on peut faire du XSS.
00:12:11En faisant cliquer l'utilisateur sur un lien piégé avec du contenu injecté,
00:12:16il pourrait voir ceci : un faux formulaire de connexion demandant de se
00:12:20reconnecter, alors que c'est une injection via le paramètre de recherche.
00:12:24L'attaquant peut exécuter du JavaScript sur la machine de la victime,
00:12:29par exemple pour voler des cookies de session et accéder à vos comptes.
00:12:33C'est un exemple classique d'échappement incorrect. On le voit mieux
00:12:37sur cette version simplifiée : je ferme la balise script pour en ouvrir
00:12:41une nouvelle avec mon propre code. L'alerte "pwned" s'affiche.
00:12:46Cela fonctionne avec un tag script Next.js en "beforeInteractive" recevant
00:12:50des données de query parameters. Next.js transforme ce tag en utilisant
00:12:55"dangerouslySetInnerHTML" — le nom est déjà un indice — et JSON.stringify.
00:12:59Il faut savoir que JSON.stringify n'échappe pas les caractères HTML
00:13:03comme les chevrons fermants. Donc nos données injectées se retrouvent
00:13:08directement dans la page, permettant de fermer prématurément le script
00:13:12légitime pour lancer le nôtre. Une petite astuce à la fin permet
00:13:16d'effacer les traces visuelles pour que l'utilisateur ne se doute de rien.
00:13:19Voilà pour ces 13 CVE de la semaine sur Next.js et React.
00:13:22Honnêtement, je ne sais plus quoi en penser. Ça m'attriste car il y a
00:13:26deux ans, je ne jurais que par Next.js, mais les obstacles s'accumulent.
00:13:31On dirait qu'ils ont précipité certaines choses avant de devoir les corriger.
00:13:34Désormais, je suis passé à TanStack et Astro pour les sites de contenu.
00:13:39C'est plus simple. J'aime aussi beaucoup ce que fait Cloudflare
00:13:43récemment, j'y migre mes projets petit à petit. Il m'en reste 20
00:13:47sur Vercel à mettre à jour. Et vous, croyez-vous aux Server Components
00:13:51ou est-ce un échec ? Dites-le-moi en commentaire, abonnez-vous,
00:13:55et à la prochaine.

Key Takeaway

La mise à jour immédiate vers les dernières versions de Next.js est impérative pour corriger 13 vulnérabilités majeures, notamment des failles SSRF, DoS et XSS liées à l'architecture des Server Components et au routage.

Highlights

  • Treize nouvelles vulnérabilités (CVE) affectent React et Next.js, dont six présentent un niveau de sévérité élevé allant jusqu'à un score de 8.6 sur 10.

  • Une faille de Server-Side Request Forgery (SSRF) permet d'accéder à des services internes protégés par un pare-feu via une simple requête curl injectant un en-tête de mise à niveau WebSocket.

  • L'utilisation de Server Components introduit un risque de déni de service (DoS) capable de ralentir les temps de réponse de 0,02 à 6 secondes via un payload de sérialisation malveillant.

  • Les applications Next.js utilisant i18n dans le Pages Router exposent des données sensibles via l'URL _next/data/ quand le middleware ne protège pas le cas de base par défaut.

  • L'empoisonnement du cache se produit lorsque Next.js stocke par erreur des données RSC en tant que HTML en raison d'une vérification d'extension de fichier contournée par une chaîne de requête.

  • L'utilisation de JSON.stringify dans dangerouslySetInnerHTML pour les scripts beforeInteractive permet l'exécution de code arbitraire car les caractères de fermeture de balise ne sont pas échappés.

Timeline

Aperçu des 13 vulnérabilités et impact système

  • Six des treize vulnérabilités découvertes sur React et Next.js sont classées comme critiques.
  • Les versions affectées incluent principalement celles utilisant le package react-server-dom.
  • TanStack Start échappe à ces vulnérabilités car il n'utilise pas les dépendances spécifiques incriminées.

Cette série de failles constitue la troisième alerte majeure concernant les Server Components cette année. Les problèmes identifiés couvrent une large gamme d'attaques, allant du déni de service au contournement de middleware. La recommandation unique pour sécuriser les applications consiste en une mise à jour globale des dépendances vers les versions corrigées.

Contournement de la protection middleware dans le Pages Router

  • L'activation de i18n avec plusieurs locales crée des chemins d'accès non protégés par le matcher du middleware.
  • L'accès direct aux fichiers JSON via l'ID de build dans _next/data/ expose les propriétés serveur normalement sécurisées.
  • La faille permet de récupérer des données sensibles telles que des e-mails ou des jetons d'accès sans authentification.

Le défaut de conception réside dans le fait que si i18n est activé, le chemin de base n'est pas systématiquement inclus dans la protection du middleware contrairement aux variantes linguistiques. Une inspection simple du code source de la page permet de trouver l'ID de build nécessaire pour reconstruire l'URL de données JSON. Next.js rappelle que le middleware ne doit pas être l'unique barrière de sécurité pour les données sensibles.

Exploitation du protocole React Flight pour le déni de service

  • Un payload de 200 000 clés aléatoires sature le thread principal lors de la désérialisation des Server Actions.
  • Le protocole React Flight effectuait initialement une complexité algorithmique de type k*n pour la comparaison des clés.
  • Le correctif réduit le nombre d'opérations de 200 millions à environ 201 000 via un système de curseur à sens unique.

L'attaquant injecte un grand nombre de pointeurs et de clés dans le payload envoyé au serveur. Avant le correctif, le serveur parcourait l'intégralité de la liste pour chaque référence, bloquant le processeur pendant plusieurs secondes. La nouvelle implémentation utilise une liste de clés indexée où le curseur ne revient jamais en arrière, garantissant un parcours unique de la structure de données.

Accès aux services internes par Server-Side Request Forgery

  • L'injection d'un en-tête Upgrade WebSocket détourne la fonction de résolution de route vers une cible externe.
  • Cette faille permet d'interroger des bases de données ou des serveurs de fichiers internes inaccessibles depuis l'internet public.
  • Le correctif introduit des gardes de statut et un booléen de validation dans la fonction resolveRoutes.

L'attaque exploite le mécanisme de proxy intégré de Next.js pour l'auto-hébergement. En fournissant une URL de service local (localhost) comme cible de requête curl, le serveur Next.js agit comme un pont et renvoie le contenu du service protégé. Le déploiement sur la plateforme Vercel n'est pas concerné par cette vulnérabilité spécifique en raison de configurations réseau différentes.

Empoisonnement de cache et Cross-Site Scripting

  • Les paramètres de requête permettent de stocker des données de composants (RSC) dans les emplacements de cache normalement réservés au HTML.
  • Les balises script beforeInteractive sont vulnérables aux injections JavaScript via les paramètres de recherche.
  • JSON.stringify ne protège pas contre l'injection de balises HTML fermantes dans le contexte de dangerouslySetInnerHTML.

L'empoisonnement de cache force les utilisateurs légitimes à recevoir du texte brut au lieu d'une page rendue. Pour la faille XSS, l'absence d'échappement des chevrons permet de fermer un script légitime et d'ouvrir une balise malveillante pour voler des cookies de session. Ces vulnérabilités soulignent les risques liés à la complexité croissante des mécanismes de rendu hybride.

Community Posts

View all posts