
Fork mantenido de node-ip con el aviso de SSRF no parcheado (CVE-2024-29415) corregido.
Fork mantenido de ip — utilidades de direcciones IP para Node.js — con el aviso SSRF sin parchear corregido.
El paquete original tiene ~38 millones de descargas/mes, sin lanzamiento desde febrero de 2024, y un aviso de gravedad ALTA (CVE-2024-29415 / GHSA-2p57-rm9w-gvfp) que afecta a todas las versiones publicadas y hace que npm audit falle en cada instalación. Este fork corrige la causa raíz en lugar de parchear expresiones regulares.
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
Las funciones originales isPrivate() / isPublic() / isLoopback() ejecutaban expresiones regulares sobre la cadena de entrada sin procesar. Cualquier representación de una dirección que las expresiones regulares no anticiparan — octetos octales o hexadecimales, IPv4 abreviada, IPv6 sin comprimir — se clasificaba silenciosamente como pública:
// 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 esas respuestas protegen solicitudes salientes, eso es un agujero SSRF (CVE-2024-29415, corrección incompleta de CVE-2023-42282).
Este fork:
net.isIP, más identificadores de zona IPv6). La clasificación se realiza sobre bytes, nunca sobre cadenas.127.1, 0x7f.1, 0177.0.0.1, 2130706433) lanzan una excepción en lugar de ser adivinadas. Fallo cerrado, no abierto. Si necesitas aceptar notaciones heredadas, normaliza explícitamente primero: ip.fromLong(ip.normalizeToLong(addr)).isPrivate() ahora significa "no alcanzable globalmente" — la misma convención que ipaddress de Python. Esto agrega CGNAT (100.64/10), TEST-NETs, benchmarking, multidifusión, reservado, ULA, rangos de documentación y más — ver la tabla a continuación.// @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 superficie de la API es la misma; las diferencias de comportamiento son las correcciones de seguridad y corrección — consulta los cambios incompatibles a continuación antes de actualizar.
Versión 3.0.0 porque la rigurosidad es la corrección — si alimentaste esta librería con cadenas no canónicas, v2 te daba respuestas incorrectas, no compatibles:
isPrivate / isPublic / isLoopback / toBuffer / toLong lanzan excepción en entradas mal formadas o no canónicas (octetos octales/hexadecimales, 127.1, cadenas de enteros). Usa normalizeToLong() primero si debes aceptar notaciones heredadas.isLoopback('fe80::1') ahora es false (enlace-local ≠ loopback; sigue siendo isPrivate). isLoopback('::') ahora es false (no especificado ≠ loopback; sigue siendo isPrivate).loopback('ipv6') devuelve en lugar de .Todo lo que exporta el original, más 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 (sin especificar, loopback, compatible con IPv4), 64:ff9b:1::/48, 100::/64, 2001::/23, 2001:db8::/32, 3fff::/20, 5f00::/16, fc00::/7, fe80::/10, fec0::/10, ff00::/8. Los rangos de transición ::ffff:0:0/96, 64:ff9b::/96 y 2002::/16 siguen su dirección IPv4 incrustada.
Si este paquete te ahorra tiempo, puedes apoyar el mantenimiento:
Bitcoin (BTC): bc1q37557q5jpeaxqydzwvf3jgj7zhnfpn2td3q40q
MIT. Basado en node-ip por Fedor Indutny.
::ffff:a.b.c.d64:ff9b::a.b.c.d2002:xxxx:xxxx::| Corregido | Problema original |
|---|
| CVE-2024-29415 / GHSA-2p57-rm9w-gvfp — SSRF a través de direcciones no canónicas | #136, #150, #153, #158 |
isPrivate() omite CGNAT, TEST-NETs, benchmarking, multidifusión, reservado, rangos de documentación | — |
cidrSubnet(...).contains() devuelve false para direcciones IPv6 mapeadas a IPv4 | #104 |
isV6Format() devuelve true para direcciones IPv4 | #67 |
isV4Format() acepta octetos ≥ 256 | #105 |
address('public') devolvía direcciones privadas y viceversa (el filtro estaba invertido) | — |
loopback('ipv6') devolvía fe80::1, que es enlace-local, no loopback → ahora ::1 | — |
cidr()/cidrSubnet() producían silenciosamente basura para longitudes de prefijo inválidas (/33, /x) → ahora lanzan excepción | — |
address(name) fallaba con TypeError para nombres de interfaz desconocidos → ahora devuelve undefined | — |
Tipos TypeScript incorporados (no se necesita @types/ip), exportaciones con nombre ESM | — |
'::1''fe80::1'isPrivate() devuelve true para más rangos (CGNAT, TEST-NETs, multidifusión, …) — ahora significa "no alcanzable globalmente".address('public') ahora devuelve direcciones públicas realmente. Si confiabas en el comportamiento invertido para obtener una dirección LAN, usa address('private') — eso es lo que te estaba dando.