CVE-2023-42282
11 Février 2025
Dernière mise à jour
11 Février 2025
Dernière mise à jour
Analyse de la faiblesse CVE-2023-42282. Cette vulnérabilité de type Server-Side Request Forgery (SSRF) affecte la dépendance ip, ce qui a conduit à son archivage temporaire.
Vendeur : N/A
Produit : ip (https://github.com/indutny/node-ip/)
Version(s) impactée(s) : < 1.1.9
Type de vulnérabilité : Server-Side Request Forgery (SSRF) (CWE-918 - Server-Side Request Forgery (SSRF))
Les versions < 1.1.9 du paquet IP sont vulnérables à une faille de type Server-Side Request Forgery (SSRF) car certaines adresses IP privées sont considérés comme publiques lors de l'appel à la méthode isPublic()
.
Le paquet IP, développé et maintenu par Fedor Indutny, permet de manipuler les adresses IP, notamment en les comparant, en convertissant leur format et en déterminant si une adresse est privée ou publiquement routable, ainsi que d'autres fonctionnalités utiles. Cette dépendance est largement utilisée, avec environ 15 millions de téléchargement par semaine.
En février 2024, la CVE-2023-42282 est enregistrée au sein de la base de données du NVD possédant un score CVSS critique de 9.8. Suite à cela, la vulnérabilité est également enregistrée dans le GHSA (le système d'avis de sécurité de GitHub).
Suite à cette remontée, le mainteneur publie un correctif dans la version 1.1.9 de la dépendance. Cependant, les retours se multiplient, alimentés par l'alerte de sécurité toujours détectée par l'outil npm audit. Suite à cela, Fedor Indutny décide d'archiver son projet et de communiquer sur les réseaux sociaux. Bien qu'il reconnaisse la présence de la vulnérabilité, il estime que sa gravité est exagérée.
A l'heure actuelle, GitHub a décidé de revoir la sévérité de la vulnérabilité, la rétrogradant de High à Low, bien qu'elle soit toujours considéeée comme Critical par le NVD. Depuis, le développeur a décidé de désarchiver son projet.
La fonction isPublic()
permet donc d'identifier si une adresse IP donnée est routable ou privée. Les adresses privées appartenant aux classes A, B et C sont les suivantes :
Classe A : de 10.0.0.0 à 10.255.255.255 (sans oublier la plage 127.0.0.0/8 pour le loopback)
Classe B: de 172.16.0.0 à 172.31.255.255
Classe C : de 192.168.0.0 à 192.168.255.255
Par exemple, l'adresse IP 10.0.0.1 est considérée comme privée, 127.0.0.1 (souvent désignée par "localhost") appartient à la plage de loopback (elle sera considérée comme privée dans la suite de cet article), tandis que l'adresse IP 8.8.8.8, utilisée pour le DNS de Google, est publique.
Le code suivant permet de tester spécifiquement le comportement de la fonction lors du traitement des adresses IP fournies dans le tableau :
Une première tentative simple peut être la suivante :
Tout se déroule correctement :
Il existe plusieurs façons de représenter la même adresse IP, notamment à travers la conversion entre IPv4 et IPv6, différents types de notations ou d'encodages, des versions raccourcies, etc. Malheureusement, la fonction isPublic()
classe incorrectement certaines adresses IP privées, encodées de manière spécifique, comme étant publiques.
Un exemple avec les addresses IP privées suivantes :
Cette liste a été élaborée en se basant sur https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Request%20Forgery/README.md.
L'adresse IP 2130706433 correspond à l'équivalent numérique de l'adresse IPv4 127.0.0.1, mais elle est considérée comme publique par la fonction. De même, le raccourci 0x7f.1 est également interprété comme une adresse publique, alors qu'il représente également 127.0.0.1.
Après avoir effectué quelques tests sur cette liste, il s'avère que les adresses o177.0.0.1, 0o177.0.0.1 et 177.0.0.1, qui sont en octal, sont également considérées comme routables par la fonction. Cependant, elles risquent de ne pas fonctionner correctement lorsqu'elles seront interprétées par certains outils, tels que cURL, Axios, Wget, ou même un navigateur.
Quel est donc le risque finalement ?
Cela pourrait poser un problème si une application dépend de la fonction isPublic()
pour autoriser ou interdire l'accès à une ressource, par exemple, hébergée sur le réseau interne (ou localhost) du système, car cela constituerait une possible exploitation d'une vulnérabilité de type SSRF.
Afin de contre balancer cette hypothèse, il est intéressant de noter que la validation de ces adresses permettent parfois de retrouver un format plus générique :
La fonction isPublic()
, présente dans le fichier ip.js de la dépendance en version 1.1.8, est la suivante :
La vulnérabilité réside donc plus précisément dans la fonction isPrivate()
, qui repose sur une série d'expressions régulières :
Si une adresse IP ne correspond à aucune des expressions régulières, elle est considérée comme publique. C'est le cas du nom d'hôte localhost ainsi que des différentes variantes d'encodages d'adresses IP privées. Cela met en évidence la difficulté de couvrir tous les cas possibles à l'aide d'expressions régulières.
L'analyse du code vulnérable indique également qu'il est essentiel pour le développeur utilisant cette fonction de valider au préalable que l'adresse IP fournie a un format correct, par exemple en utilisant la librairie net et la fonction isIP de Node.js.
Un correctif a été apporté dans la version 1.1.9, publiée le 19 février 2024. La nouvelle version de la fonction isPrivate()
est la suivante :
Tout d'abord, la liste des expressions régulières se voit amputer de celle-ci (qui permettait d'identifier les adresses IP commençant par 127 ou ::ffff:) :
Vient ensuite l'ajout de l'appel à la fonction isLoopback()
, qui utilise un ensemble d'expressions régulières dont celle précédemment supprimée :
Cette fonction existait déjà dans la version 1.1.8 mais a été enrichie par la version 1.1.9.
La conversion du format décimal permet de convertir les adresses IP, comme 2130706433, en leur équivalent au format IPv4 :
Cependant, elle ne transforme pas correctement les adresses au format octal :
Les tests des expressions régulières effectués ensuite par la fonction ne changera rien, et l'adresse IP au format octal sera finalement considérée comme n'appartenant pas au loopback/localhost.
L'appel à la fonction isV6Format()
permet de s'assurer qu'une adresse IPv4 (hors localhost, car déjà traité par la partie précédepent) possède un format valide. Pour cela, elle ne doit pas correspondre à l'expression régulière suivante, qui représente une adresse IPv6 :
La fonction normalizeToLong()
prend en entrée une adresse IPv4 et tente de la convertir en un entier normalisé en tenant compte de son format (octal, hexadécimal, décimal). our cela, elle divise la chaîne à chaque occurrence du caractère " . " :
Par exemple, l'adresse représentée en hexadécimal sous la forme 0x0A.0x00.0x00.0x01 (correspondand à 10.0.0.1), sera convertie en sa valeur entière : 167772161.
Le dernier appel effectué est l'appel à la fonction fromLong()
qui extrait les quatre octets de la valeur entière précédemment convertit :
Résultant finalement en 10.0.0.1.
Malheureusement, ce correctif reste insuffisant, et certaines adresses IP privées, notamment celles représentant la boucle locale, sont toujours considérées comme routables, ce qui les rend exploitables dans le cadre d'attaques SSRF.
Pour le vérifier, il suffit de reprendre la liste des adresses IP testées sur la version 1.1.8 du paquet et de les tester à nouveau avec le correctif de la version 1.1.9 :
Certaines adresses ne sont désormais plus reconnues comme valides :
D'autres sont désormais correctement identifiées comme privées :
Certaines continuent d'être incorrectement classées :
Et une régression est même apparue :
Une image Docker illustrant une vulnérabilité SSRF, où le contrôle d'accès est effectué à l'aide de la dépendance ip, est disponible ici.
Cette vulnérabilité présente un intérêt technique intéressant, tant du côté de l'attaque, où différentes méthodes peuvent conduire aux mêmes résultats, que du côté de la défense, où la complexité de prendre en compte tous les scénarios possibles lors du traitement de données non fiables devient manifeste.
Toutefois, ce qui s'avère encore plus fascinant, c'est la réflexion qu'elle suscite sur le principe même de l'open-source. Une telle dépendance n'a peut-être pas été conçue à l'origine pour être utilisée à des fins de sécurité par son auteur, mais rien n'empêche un utilisateur de l'exploiter ainsi. Et comment le savoir, dans ce cas ? Dès lors, se pose la question de savoir si ces failles de sécurité sont véritablement de la seule responsabilité du mainteneur. Dans le cadre des projets open-source ou des projets soutenus par une entreprise, cette responsabilité semble plus claire. Mais qu'en est-il lorsque le développeur œuvre seul, sur son temps libre ? Est-il, dans ce cas, responsable des vulnérabilités remontées et doit-il s'engager à les corriger dans un délai précis ? Doit-il répondre à toutes les sollicitations, fournir des informations détaillées sur la vulnérabilité et offrir des correctifs, comme cela a été le cas pour Fedor Indutny ? Ces questions invitent à une réflexion plus profonde sur la responsabilité dans le monde de l'open-source.