
Fork mantenuto di node-ip in cui è stato risolto l'avviso SSRF senza patch (CVE-2024-29415)
Fork mantenuto di ip — utility per indirizzi IP per Node.js — con l'avviso SSRF non corretto risolto.
Il pacchetto originale ha ~38M download/mese, nessuna release da febbraio 2024 e un avviso di gravità ALTA (CVE-2024-29415 / GHSA-2p57-rm9w-gvfp) che riguarda ogni versione pubblicata e fa fallire npm audit a ogni installazione. Questo fork corregge la causa principale invece di applicare patch alle 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
Le funzioni originali isPrivate() / isPublic() / isLoopback() eseguivano regex sulla stringa di input grezza. Qualsiasi variante di un indirizzo che le regex non avevano previsto — ottetti ottali o esadecimali, IPv4 abbreviato, IPv6 non compresso — veniva silenziosamente classificata come pubblica:
// ip originale @2.0.1 — tutti questi sono loopback/privati, tutti segnalati come PUBLIC:
ip.isPublic('127.1') // true 💥
ip.isPublic('012.1.2.3') // true 💥 (10.1.2.3 in ottale)
ip.isPublic('01200034567') // true 💥 (intero 10.0.14.119)
ip.isPublic('000:0:0000::01') // true 💥 (cioè ::1)
Se queste risposte proteggono le richieste in uscita, si tratta di una falla SSRF (CVE-2024-29415, correzione incompleta di CVE-2023-42282).
Questo fork:
net.isIP, più gli zone ID IPv6). La classificazione opera sui byte, mai sulle stringhe.127.1, 0x7f.1, 0177.0.0.1, 2130706433) generano un'eccezione invece di essere interpretate a indovinello. Fail closed, non open. Se devi accettare notazioni legacy, normalizza prima esplicitamente: ip.fromLong(ip.normalizeToLong(addr)).isPrivate() ora significa "non raggiungibile globalmente" — la stessa convenzione di ipaddress di Python. Questo aggiunge CGNAT (100.64/10), TEST-NET, benchmarking, multicast, reserved, ULA, range di documentazione e altro — vedi la tabella sotto.// @bybrave/ip2
ip.isPublic('127.1') // throws Error: Invalid ip address: 127.1
ip.isPrivate('000:0:0000::01') // true (interpretato come ::1)
ip.isLoopback('::fFFf:127.0.0.1')// true
ip.isPublic('64:ff9b::8.8.8.8') // true (NAT64 di un indirizzo pubblico)
ip.isPrivate('100.64.0.1') // true (CGNAT — l'originale diceva public)
[email protected]- const ip = require('ip');
+ const ip = require('@bybrave/ip2');
La superficie API è la stessa; le differenze comportamentali sono le correzioni di sicurezza e di correttezza — vedi le modifiche incompatibili sotto prima di aggiornare.
Versione 3.0.0 perché il rigore è la correzione — se passavi a questa libreria stringhe non canoniche, la v2 ti dava risposte sbagliate, non compatibili:
isPrivate / isPublic / isLoopback / toBuffer / toLong generano un'eccezione su input malformati o non canonici (ottetti ottali/esadecimali, 127.1, stringhe intere). Usa normalizeToLong() prima se devi accettare notazioni legacy.isLoopback('fe80::1') ora è false (link-local ≠ loopback; resta isPrivate). isLoopback('::') ora è false (unspecified ≠ loopback; resta isPrivate).loopback('ipv6') restituisce invece di .Tutto ciò che esporta l'originale, più 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. broadcast).
IPv6: ::/96 (unspecified, 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. I range di transizione ::ffff:0:0/96, 64:ff9b::/96 e 2002::/16 seguono il loro indirizzo IPv4 incorporato.
Se questo pacchetto ti fa risparmiare tempo, puoi sostenere la manutenzione:
Bitcoin (BTC): bc1q37557q5jpeaxqydzwvf3jgj7zhnfpn2td3q40q
MIT. Basato su node-ip di Fedor Indutny.
::ffff:a.b.c.d64:ff9b::a.b.c.d2002:xxxx:xxxx::| Corretto | Problema originale |
|---|
| CVE-2024-29415 / GHSA-2p57-rm9w-gvfp — SSRF tramite indirizzi non canonici | #136, #150, #153, #158 |
isPrivate() non copre CGNAT, TEST-NET, benchmarking, multicast, reserved, range di documentazione | — |
cidrSubnet(...).contains() restituisce false per indirizzi IPv6 mappati IPv4 | #104 |
isV6Format() restituisce true per indirizzi IPv4 | #67 |
isV4Format() accetta ottetti ≥ 256 | #105 |
address('public') restituiva indirizzi privati e viceversa (filtro invertito) | — |
loopback('ipv6') restituiva fe80::1, che è link-local, non loopback → ora ::1 | — |
cidr()/cidrSubnet() producevano silenziosamente risultati errati per prefissi non validi (/33, /x) → ora generano un'eccezione | — |
address(name) crashava con TypeError per nomi di interfaccia sconosciuti → ora restituisce undefined | — |
Tipi TypeScript inclusi (niente @types/ip), export ESM nominati | — |
'::1''fe80::1'isPrivate() restituisce true per più range (CGNAT, TEST-NET, multicast, …) — ora significa "non raggiungibile globalmente".address('public') ora restituisce davvero indirizzi pubblici. Se facevi affidamento sul comportamento invertito per ottenere un indirizzo LAN, usa address('private') — era quello che ti restituiva.