> For the complete documentation index, see [llms.txt](https://sharpforce.gitbook.io/cybersecurity/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sharpforce.gitbook.io/cybersecurity/cve/2023/cve-2023-42282.md).

# CVE-2023-42282

> 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.

## Description

* **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)](https://cwe.mitre.org/data/definitions/918.html))

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()`.

<figure><img src="/files/2S7Ti1HyARmKbUjnPbtJ" alt=""><figcaption></figcaption></figure>

## Préambule

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.

## Analyse de la vulnérabilité

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 :&#x20;

* **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 :&#x20;

```javascript
ip = require("ip-1.1.8");
const ips_addresses = [];

ips_addresses.forEach(ip_address => {
    // Nécessaire pour supprimer les "[" et "]" qui entourent les IPv6
    if (ip_address.startsWith("[") && ip_address.endsWith("]")) {
        ip_address = ip_address.slice(1, -1);
    }

    if (ip.isPublic(ip_address)) {
        console.log(`${ip_address} is public`);
    } else {
        console.log(`${ip_address} is private`);
    }
});
```

Une première tentative simple peut être la suivante :&#x20;

```javascript
const ips_addresses = ['8.8.8.8', '127.0.0.1', '10.0.0.1'];
```

Tout se déroule correctement :&#x20;

```bash
8.8.8.8 is public
127.0.0.1 is private
10.0.0.1 is private
```

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 :&#x20;

```javascript
const ips_addresses = [
    'localhost', '127.0.0.1', '0.0.0.0', '[::]', '[0000::1]',
    '[0:0:0:0:0:ffff:127.0.0.1]', '[::ffff:127.0.0.1]', '[::fFFf:127.0.0.1]',
    '127.127.127.127', '127.0.1.3', '127.0.0.0', '127.0.0.2', '127.1.1.1',
    '0', '0x7f.1', '127.1', '127.0.1', '[000:0:0000::01]', '0177.0.0.1',
    '0x7F.0x00.0x00.0x01', '127.0.0.1/32', '2130706433',
    '017700000001', '0177.0.0.1', 'o177.0.0.1', '0o177.0.0.1', 'q177.0.0.1'
];
```

{% hint style="info" %}
Cette liste a été élaborée en se basant sur  <https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Request%20Forgery/README.md>.
{% endhint %}

```bash
localhost is public
127.0.0.1 is private
0.0.0.0 is public
:: is private
0000::1 is public
0:0:0:0:0:ffff:127.0.0.1 is public
::ffff:127.0.0.1 is private
::fFFf:127.0.0.1 is private
127.127.127.127 is private
127.0.1.3 is private
127.0.0.0 is private
127.0.0.2 is private
127.1.1.1 is private
0 is public
0x7f.1 is public
127.1 is public
127.0.1 is public
000:0:0000::01 is public
0177.0.0.1 is public
0x7F.0x00.0x00.0x01 is public
127.0.0.1/32 is public
2130706433 is public
017700000001 is public
0177.0.0.1 is public
o177.0.0.1 is public
0o177.0.0.1 is public
q177.0.0.1 is public
```

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 :&#x20;

```javascript
let parsedUrl = new URL("http://0x7f.1/");
let hostname = parsedUrl.hostname;
console.log(hostname); // Affichera 127.0.0.1
```

## Code vulnérable

La fonction `isPublic()`, présente dans le fichier ip.js de la dépendance en version 1.1.8, est la suivante :&#x20;

```javascript
ip.isPublic = function (addr) {
  return !ip.isPrivate(addr);
};
```

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 :

```javascript
ip.isPrivate = function (addr) {
  return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i
    .test(addr)
    || /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i
      .test(addr)
    || /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^f[cd][0-9a-f]{2}:/i.test(addr)
    || /^fe80:/i.test(addr)
    || /^::1$/.test(addr)
    || /^::$/.test(addr);
};
```

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.

## Correction

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 :&#x20;

```javascript
ip.isPrivate = function (addr) {
  // check loopback addresses first
  if (ip.isLoopback(addr)) {
    return true;
  }

  // ensure the ipv4 address is valid
  if (!ip.isV6Format(addr)) {
    const ipl = ip.normalizeToLong(addr);
    if (ipl < 0) {
      throw new Error('invalid ipv4 address');
    }
    // normalize the address for the private range checks that follow
    addr = ip.fromLong(ipl);
  }

  // check private ranges
  return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i
      .test(addr)
    || /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
    || /^f[cd][0-9a-f]{2}:/i.test(addr)
    || /^fe80:/i.test(addr)
    || /^::1$/.test(addr)
    || /^::$/.test(addr);
};
```

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:) :

```regex
/^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i
```

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 :

```javascript
ip.isLoopback = function (addr) {
  // If addr is an IPv4 address in long integer form (no dots and no colons), convert it
  if (!/\./.test(addr) && !/:/.test(addr)) {
    addr = ip.fromLong(Number(addr));
  }

  return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/
    .test(addr)
    || /^0177\./.test(addr)
    || /^0x7f\./i.test(addr)
    || /^fe80::1$/i.test(addr)
    || /^::1$/.test(addr)
    || /^::$/.test(addr);
};
```

{% hint style="info" %}
Cette fonction existait déjà dans la version 1.1.8 mais a été enrichie par la version 1.1.9.
{% endhint %}

La conversion du format décimal permet de convertir les adresses IP, comme 2130706433, en leur équivalent au format IPv4 :

```javascript
ip.fromLong = function (ipl) {
  return (`${ipl >>> 24}.${
    ipl >> 16 & 255}.${
    ipl >> 8 & 255}.${
    ipl & 255}`);
};
```

```javascript
let addr = "2130706433";

if (!/\./.test(addr) && !/:/.test(addr)) {
    addr = ip.fromLong(Number(addr));
    console.log(addr) // Affichera 127.0.0.1
}
```

Cependant, elle ne transforme pas correctement les adresses au format octal :&#x20;

```javascript
let addr = "017700000001";

if (!/\./.test(addr) && !/:/.test(addr)) {
    addr = ip.fromLong(Number(addr));
    console.log(addr) // Affichera 31.0.145.1
}
```

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 :

```javascript
var ipv6Regex = /^(::)?(((\d{1,3}\.){3}(\d{1,3}){1})?([0-9a-f]){0,4}:{0,2}){1,8}(::)?$/i;
```

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 " . " :&#x20;

```javascript
ip.normalizeToLong = function (addr) {
  const parts = addr.split('.').map(part => {
    // Handle hexadecimal format
    if (part.startsWith('0x') || part.startsWith('0X')) {
      return parseInt(part, 16);
    }
    // Handle octal format (strictly digits 0-7 after a leading zero)
    else if (part.startsWith('0') && part !== '0' && /^[0-7]+$/.test(part)) {
      return parseInt(part, 8);
    }
    // Handle decimal format, reject invalid leading zeros
    else if (/^[1-9]\d*$/.test(part) || part === '0') {
      return parseInt(part, 10);
    }
    // Return NaN for invalid formats to indicate parsing failure
    else {
      return NaN;
    }
  });
```

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.&#x20;

Le dernier appel effectué est l'appel à la fonction `fromLong()` qui extrait les quatre octets de la valeur entière précédemment convertit :&#x20;

```javascript
ip.fromLong = function (ipl) {
  return (`${ipl >>> 24}.${
    ipl >> 16 & 255}.${
    ipl >> 8 & 255}.${
    ipl & 255}`);
};
```

Résultant finalement en 10.0.0.1.

## Contournement du correctif

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 :&#x20;

```javascript
const ips_addresses = [
    'localhost', '127.0.0.1', '0.0.0.0', '[::]', '[0000::1]',
    '[0:0:0:0:0:ffff:127.0.0.1]', '[::ffff:127.0.0.1]', '[::fFFf:127.0.0.1]',
    '127.127.127.127', '127.0.1.3', '127.0.0.0', '127.0.0.2', '127.1.1.1',
    '0', '0x7f.1', '127.1', '127.0.1', '[000:0:0000::01]', '0177.0.0.1',
    '0x7F.0x00.0x00.0x01', '127.0.0.1/32', '2130706433',
    '017700000001', '0177.0.0.1', 'o177.0.0.1', '0o177.0.0.1', 'q177.0.0.1'
];
```

Certaines adresses ne sont désormais plus reconnues comme valides :&#x20;

```bash
localhost - Error: Error: invalid ipv4 address
o177.0.0.1 - Error: Error: invalid ipv4 address
0o177.0.0.1 - Error: Error: invalid ipv4 address
q177.0.0.1 - Error: Error: invalid ipv4 address
```

D'autres sont désormais correctement identifiées comme privées :&#x20;

```bash
0x7f.1 is private
0177.0.0.1 is private
0x7F.0x00.0x00.0x01 is private
127.0.0.1/32 is private
2130706433 is private
0177.0.0.1 is private
```

Certaines continuent d'être incorrectement classées :&#x20;

```bash
0.0.0.0 is public
0000::1 is public
0:0:0:0:0:ffff:127.0.0.1 is public
0 is public
127.1 is public
127.0.1 is public
000:0:0000::01 is public
017700000001 is public
```

Et une régression est même apparue :&#x20;

```bash
::fFFf:127.0.0.1 is public
```

## Labs

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](https://github.com/Sharpforce/cybersecurity-code/tree/main/CVE-2023-42282).

<figure><img src="/files/wCb5ZJJpacTzCys7Ezeh" alt=""><figcaption></figcaption></figure>

## Conclusion

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.

## Ressources

* <https://github.com/indutny/node-ip/>
* <https://programmation.developpez.com/actu/359875/Un-developpeur-contraint-d-archiver-le-depot-GitHub-de-son-projet-apres-une-avalanche-de-rapports-douteux-sur-une-vulnerabilite-il-denonce-une-tendance-qui-peut-nuire-a-la-reputation-des-projets-open-source/>
* <https://github.com/github/advisory-database/blob/1116380c2d45a6aa5aef36845fde10087ee70ac0/advisories/github-reviewed/2024/02/GHSA-78xj-cgh5-2h22/GHSA-78xj-cgh5-2h22.json>
* <https://github.com/github/advisory-database/blob/49fc787f17bfab369776763fa0407372620b12b0/advisories/github-reviewed/2024/02/GHSA-78xj-cgh5-2h22/GHSA-78xj-cgh5-2h22.json>
* <https://nvd.nist.gov/vuln/detail/CVE-2024-29415>
* <https://github.com/advisories/GHSA-78xj-cgh5-2h22>
* <https://security.snyk.io/vuln/SNYK-JS-IP-6240864>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://sharpforce.gitbook.io/cybersecurity/cve/2023/cve-2023-42282.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
