
Fork maintenu de node-ip ayant corrigé l'avis SSRF non corrigé (CVE-2024-29415).
Fork maintenu de ip — utilitaires d'adresse IP pour Node.js — avec l'avis SSRF non corrigé résolu.
Le package original a ~38 millions de téléchargements/mois, aucune version depuis février 2024, et un avis de sévérité ÉLEVÉE (CVE-2024-29415 / GHSA-2p57-rm9w-gvfp) qui affecte chaque version publiée et fait échouer npm audit à chaque installation. Ce fork corrige la cause racine au lieu de patcher les expressions régulières.
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 orthographe d'une adresse que les regex n'avaient pas anticipée — octets octaux ou hexadécimaux, IPv4 abrégé, IPv6 non compressé — é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 les requêtes sortantes, c'est un trou SSRF (CVE-2024-29415, correction incomplète de CVE-2023-42282).
Ce fork :
net.isIP, plus ID de zone IPv6). La classification s'effectue sur les octets, jamais sur les chaînes.127.1, 0x7f.1, 0177.0.0.1, 2130706433) lèvent une erreur au lieu d'être devinées. Échouer en mode fermé, pas ouvert. Si vous devez accepter des notations héritées, normalisez explicitement d'abord : ip.fromLong(ip.normalizeToLong(addr)).isPrivate() signifie maintenant « non joignable globalement » — la même convention que ipaddress de Python. Cela ajoute CGNAT (100.64/10), TEST-NETs, benchmarking, multicast, réservé, ULA, plages de documentation et plus — voir le tableau ci-dessous.// @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]- const ip = require('ip');
+ const ip = require('@bybrave/ip2');
La surface API est la même ; les différences de comportement sont les correctifs de sécurité et d'exactitude — voir les changements cassants ci-dessous avant de mettre à niveau.
Version 3.0.0 car la rigueur est le correctif — si vous donniez à cette bibliothèque des chaînes non canoniques, v2 vous donnait de mauvaises réponses, pas des réponses compatibles :
isPrivate / isPublic / isLoopback / toBuffer / toLong lèvent une erreur sur une entrée malformée ou non canonique (octets octaux/hex, 127.1, chaînes entières). Utilisez normalizeToLong() d'abord si vous devez accepter des notations héritées.isLoopback('fe80::1') est maintenant false (link-local ≠ loopback ; toujours isPrivate). isLoopback('::') est maintenant false (non spécifié ≠ loopback ; toujours isPrivate).loopback('ipv6') renvoie au lieu de .Everything the original exports, 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'
const s = ip.cidrSubnet('192.168.1.134/26')
s.networkAddress // '192.168.1.128'
s.firstAddress // '192.168.1.129'
s.lastAddress // '192.168.1.190'
s.broadcastAddress // '192.168.1.191'
s.subnetMaskLength // 26
s.numHosts // 62
s.contains('::ffff:192.168.1.180') // true (v4-mapped handled)
IPv4 : 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10 (CGNAT), 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.88.99.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4 (incl. diffusion).
IPv6 : ::/96 (non spécifié, loopback, v4-compatible), 64:ff9b:1::/48, 100::/64, 2001::/23, 2001:db8::/32, 3fff::/20, 5f00::/16, fc00::/7, fe80::/10, fec0::/10, ff00::/8. Les plages de transition ::ffff:0:0/96, 64:ff9b::/96 et 2002::/16 suivent leur adresse IPv4 intégrée.
Si ce package vous fait gagner du temps, vous pouvez soutenir la maintenance :
Bitcoin (BTC) : bc1q37557q5jpeaxqydzwvf3jgj7zhnfpn2td3q40q
MIT. Basé sur node-ip de Fedor Indutny.
::ffff:a.b.c.d64:ff9b::a.b.c.d2002:xxxx:xxxx::| Correctif | Problème d'origine |
|---|
| CVE-2024-29415 / GHSA-2p57-rm9w-gvfp — SSRF via des adresses non canoniques | #136, #150, #153, #158 |
isPrivate() oublie CGNAT, TEST-NETs, benchmarking, multicast, réservé, plages de documentation | — |
cidrSubnet(...).contains() renvoie false pour les adresses IPv6 mappées IPv4 | #104 |
isV6Format() renvoie true pour les adresses IPv4 | #67 |
isV4Format() accepte les octets ≥ 256 | #105 |
address('public') renvoyait des adresses privées et vice versa (le filtre était inversé) | — |
loopback('ipv6') renvoyait fe80::1, qui est link-local, pas loopback → maintenant ::1 | — |
cidr()/cidrSubnet() produisait silencieusement des déchets pour des longueurs de préfixe invalides (/33, /x) → maintenant lève une erreur | — |
address(name) plantait avec TypeError pour des noms d'interface inconnus → maintenant renvoie undefined | — |
Types TypeScript intégrés (pas besoin de @types/ip), exports nommés ESM | — |
'::1''fe80::1'isPrivate() renvoie true pour plus de plages (CGNAT, TEST-NETs, multicast, …) — elle signifie maintenant « non joignable globalement ».address('public') renvoie maintenant effectivement 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.