
Fork maintenu de node-ip ayant corrigé l'avis SSRF non corrigé (CVE-2024-29415).
Fork maintenu de ip — utilitaires d'adresses IP pour Node.js — avec l'avis SSRF non corrigé résolu.
Le paquet original compte ~38M de téléchargements/mois, aucune version depuis février 2024, et un avis de gravité ÉLEVÉE (CVE-2024-29415 / GHSA-2p57-rm9w-gvfp) qui affecte toutes les versions publiées et fait échouer npm audit à chaque installation. Ce fork corrige la cause racine au lieu de patcher des regex.
npm install @bybrave/ip2
// CommonJS — drop-in
const ip = require('@bybrave/ip2');
// ESM — named imports
import { isPrivate, isPublic, cidrSubnet } from '@bybrave/ip2';
// TypeScript types built in
Les fonctions originales isPrivate() / isPublic() / isLoopback() exécutaient des regex sur la chaîne d'entrée brute. Toute écriture d'une adresse que les regex n'anticipaient pas — octets en octal ou hexadécimal, IPv4 abrégée, IPv6 non compressée — était silencieusement classée comme publique :
// original [email protected] — all of these are loopback/private, all reported PUBLIC:
ip.isPublic('127.1') // true 💥
ip.isPublic('012.1.2.3') // true 💥 (octal 10.1.2.3)
ip.isPublic('01200034567') // true 💥 (integer 10.0.14.119)
ip.isPublic('000:0:0000::01') // true 💥 (that's ::1)
Si ces réponses protègent des requêtes sortantes, c'est une faille SSRF (CVE-2024-29415, correctif incomplet de CVE-2023-42282).
Ce fork :
net.isIP, plus les identifiants de zone IPv6). La classification s'exécute sur les octets, jamais sur les chaînes.127.1, 0x7f.1, 0177.0.0.1, 2130706433) lèvent une exception au lieu d'être devinées. Échouer en fermant, pas en ouvrant. Si vous devez accepter des notations héritées, normalisez-les explicitement d'abord : ip.fromLong(ip.normalizeToLong(addr)).isPrivate() signifie désormais « non joignable globalement » — la même convention que ipaddress de Python. Cela ajoute CGNAT (100.64/10), les TEST-NET, le benchmarking, le multicast, les plages réservées, ULA, les plages de documentation et plus encore — voir le tableau ci-dessous.::ffff:a.b.c.d (v4-mapped), 64:ff9b::a.b.c.d (NAT64) et 2002:xxxx:xxxx:: (6to4) sont privées/publiques selon l'adresse IPv4 qu'elles contiennent.// @bybrave/ip2
ip.isPublic('127.1') // throws Error: Invalid ip address: 127.1
ip.isPrivate('000:0:0000::01') // true (parsed as ::1)
ip.isLoopback('::fFFf:127.0.0.1')// true
ip.isPublic('64:ff9b::8.8.8.8') // true (NAT64 of a public address)
ip.isPrivate('100.64.0.1') // true (CGNAT — original said public)
[email protected]| Corrigé | Problème d'origine |
|---|---|
| CVE-2024-29415 / GHSA-2p57-rm9w-gvfp — SSRF via des adresses non canoniques | #136, #150, #153, #158 |
isPrivate() manque CGNAT, les TEST-NET, le benchmarking, le multicast, les plages réservées, de documentation | — |
cidrSubnet(...).contains() renvoie false pour les adresses IPv6 mappées IPv4 | #104 |
isV6Format() renvoie true pour les adresses IPv4 | #67 |
isV4Format() accepte des octets ≥ 256 | #105 |
address('public') renvoyait des adresses privées et inversement (le filtre était inversé) | — |
loopback('ipv6') renvoyait fe80::1, qui est link-local, pas loopback → désormais ::1 | — |
cidr()/cidrSubnet() produisaient silencieusement des résultats erronés pour des longueurs de préfixe invalides (/33, /x) → lèvent désormais une exception | — |
address(name) plantait avec TypeError pour des noms d'interface inconnus → renvoie désormais undefined | — |
cidrSubnet()/subnet() donnaient des réponses fausses pour IPv6 (/48 contenait un /48 voisin) → véritable arithmétique de sous-réseau IPv6 | #89, #110 |
Le bundling pour le navigateur échouait sur os (webpack 5) → le champ browser le redirige | #127, #132 |
Types TypeScript intégrés (pas besoin de @types/ip), exports nommés ESM | — |
- const ip = require('ip');
+ const ip = require('@bybrave/ip2');
La surface d'API est identique ; les différences de comportement sont les corrections de sécurité et d'exactitude — consultez les changements majeurs ci-dessous avant de mettre à jour.
Version 3.0.0 car la rigueur est la correction — si vous fournissiez à cette bibliothèque des chaînes non canoniques, la v2 vous donnait des réponses fausses, pas compatibles :
isPrivate / isPublic / isLoopback / toBuffer / toLong lèvent une exception sur une entrée malformée ou non canonique (octets en octal/hexadécimal, 127.1, chaînes d'entiers). Utilisez normalizeToLong() d'abord si vous devez accepter des notations héritées.isLoopback('fe80::1') est désormais false (link-local ≠ loopback ; reste isPrivate). isLoopback('::') est désormais false (non spécifiée ≠ loopback ; reste isPrivate).loopback('ipv6') renvoie '::1' au lieu de 'fe80::1'.isPrivate() renvoie true pour davantage de plages (CGNAT, TEST-NET, multicast, …) — cela signifie désormais « non joignable globalement ».address('public') renvoie désormais réellement des adresses publiques. Si vous comptiez sur le comportement inversé pour obtenir une adresse LAN, utilisez address('private') — c'est ce qu'elle vous donnait.Tout ce que l'original exporte, plus isValid() :
ip.address('public', 'ipv6') // address of a network interface
ip.isPrivate('10.0.0.1') // true — not globally reachable
ip.isPublic('8.8.8.8') // true
ip.isLoopback('127.8.8.8') // true
ip.isValid('300.1.2.3') // false (never throws)
ip.isV4Format('192.168.0.1') // true
ip.isV6Format('2001:db8::1') // true
ip.isEqual('::ffff:7f00:1', '127.0.0.1') // true
ip.toBuffer('127.0.0.1') // Buffer([127, 0, 0, 1])
ip.toString(buf, offset, length) // '127.0.0.1'
ip.toLong('127.0.0.1') // 2130706433
ip.fromLong(2130706433) // '127.0.0.1'
ip.normalizeToLong('127.1') // 2130706433 (lenient, -1 on error)
ip.fromPrefixLen(24) // '255.255.255.0'
ip.mask('192.168.1.134', '255.255.255.0') // '192.168.1.0'
ip.cidr('192.168.1.134/26') // '192.168.1.128'
ip.not('255.255.255.0') // '0.0.0.255'
ip.or('0.0.0.255', '192.168.1.10') // '192.168.1.255'