
Fork mantenuto di node-ip in cui è stato risolto l'avviso SSRF senza patch (CVE-2024-29415)
Fork mantenuto di ip — utilità per indirizzi IP per Node.js — con la vulnerabilità SSRF non corretta risolta.
Il pacchetto originale ha ~38M download/mese, nessuna release da febbraio 2024, e un advisory di gravità HIGH (CVE-2024-29415 / GHSA-2p57-rm9w-gvfp) che interessa ogni versione pubblicata e fa fallire npm audit a ogni installazione. Questo fork corregge la causa alla radice invece di rattoppare le 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 grafia di un indirizzo che le regex non prevedevano — ottetti ottali o esadecimali, IPv4 abbreviato, IPv6 non compresso — veniva silenziosamente classificata come pubblica:
// 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)
Se queste risposte proteggono 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 indovinate. Fail closed, non open. Se devi accettare notazioni legacy, normalizza esplicitamente prima: 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.::ffff:a.b.c.d (v4-mapped), 64:ff9b::a.b.c.d (NAT64) e 2002:xxxx:xxxx:: (6to4) sono privati/pubblici in base all'indirizzo IPv4 al loro interno.// @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]| Corretto | Problema originale |
|---|---|
| CVE-2024-29415 / GHSA-2p57-rm9w-gvfp — SSRF tramite indirizzi non canonici | #136, #150, #153, #158 |
isPrivate() non rileva CGNAT, TEST-NET, benchmarking, multicast, reserved, range di documentazione | — |
cidrSubnet(...).contains() restituisce false per indirizzi IPv6 v4-mapped | #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 lunghezze di prefisso non valide (/33, /x) → ora generano un'eccezione | — |
address(name) andava in crash con TypeError per nomi di interfaccia sconosciuti → ora restituisce undefined | — |
cidrSubnet()/subnet() davano risposte errate per IPv6 (/48 conteneva un /48 adiacente) → vera aritmetica di subnet IPv6 | #89, #110 |
Il bundling per il browser falliva su os (webpack 5) → il campo browser lo mappa altrove | #127, #132 |
Tipi TypeScript integrati (nessun @types/ip necessario), export nominati ESM | — |
- const ip = require('ip');
+ const ip = require('@bybrave/ip2');
La superficie API è la stessa; le differenze di comportamento sono le correzioni di sicurezza e correttezza — vedi le breaking changes 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 prima normalizeToLong() se devi accettare notazioni legacy.isLoopback('fe80::1') ora è false (link-local ≠ loopback; resta comunque isPrivate). isLoopback('::') ora è false (unspecified ≠ loopback; resta comunque isPrivate).loopback('ipv6') restituisce '::1' invece di 'fe80::1'.isPrivate() restituisce true per più range (CGNAT, TEST-NET, multicast, …) — ora significa "non raggiungibile globalmente".address('public') ora restituisce effettivamente indirizzi pubblici. Se facevi affidamento sul comportamento invertito per ottenere un indirizzo LAN, usa address('private') — era quello che ti restituiva.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'