
Fork mantido do node-ip com o aviso SSRF (CVE-2024-29415) corrigido.
Fork mantido de ip — utilitários de endereços IP para Node.js — com o aviso de SSRF não corrigido resolvido.
O pacote original tem ~38M downloads/mês, nenhuma versão desde fevereiro de 2024, e um aviso de severidade ALTA (CVE-2024-29415 / GHSA-2p57-rm9w-gvfp) que afeta todas as versões publicadas e faz o npm audit falhar em toda instalação. Este fork corrige a causa raiz em vez de aplicar patches em regexes.
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
As funções originais isPrivate() / isPublic() / isLoopback() executavam regexes sobre a string de entrada bruta. Qualquer grafia de um endereço que as regexes não previam — octetos octais ou hexadecimais, IPv4 abreviado, IPv6 não comprimido — era silenciosamente classificada 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)
Se essas respostas protegem requisições de saída, isso é uma brecha de SSRF (CVE-2024-29415, correção incompleta da CVE-2023-42282).
Este fork:
net.isIP, mais zone IDs de IPv6). A classificação é executada sobre bytes, nunca sobre strings.127.1, 0x7f.1, 0177.0.0.1, 2130706433) lançam exceção em vez de serem adivinhadas. Falha fechada, não aberta. Se você precisa aceitar notações legadas, normalize explicitamente primeiro: ip.fromLong(ip.normalizeToLong(addr)).isPrivate() agora significa "não globalmente acessível" — a mesma convenção do ipaddress do Python. Isso adiciona CGNAT (100.64/10), TEST-NETs, benchmarking, multicast, reservados, ULA, faixas de documentação e mais — veja a tabela abaixo.::ffff:a.b.c.d (v4-mapped), 64:ff9b::a.b.c.d (NAT64) e 2002:xxxx:xxxx:: (6to4) são privados/públicos de acordo com o endereço IPv4 dentro deles.// @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]| Corrigido | Problema original |
|---|---|
| CVE-2024-29415 / GHSA-2p57-rm9w-gvfp — SSRF via endereços não canônicos | #136, #150, #153, #158 |
isPrivate() não cobre CGNAT, TEST-NETs, benchmarking, multicast, reservados, faixas de documentação | — |
cidrSubnet(...).contains() retorna false para endereços IPv6 mapeados de IPv4 | #104 |
isV6Format() retorna true para endereços IPv4 | #67 |
isV4Format() aceita octetos ≥ 256 | #105 |
address('public') retornava endereços privados e vice-versa (filtro estava invertido) | — |
loopback('ipv6') retornava fe80::1, que é link-local, não loopback → agora ::1 | — |
cidr()/cidrSubnet() produziam silenciosamente lixo para comprimentos de prefixo inválidos (/33, /x) → agora lançam exceção | — |
address(name) travava com TypeError para nomes de interface desconhecidos → agora retorna undefined | — |
cidrSubnet()/subnet() davam respostas erradas para IPv6 (/48 continha um /48 vizinho) → matemática real de sub-redes IPv6 | #89, #110 |
Empacotamento para o navegador falhava em os (webpack 5) → campo browser o mapeia para fora | #127, #132 |
Tipos TypeScript embutidos (sem necessidade de @types/ip), exportações nomeadas ESM | — |
- const ip = require('ip');
+ const ip = require('@bybrave/ip2');
A superfície da API é a mesma; as diferenças de comportamento são as correções de segurança e de correção — veja as mudanças incompatíveis abaixo antes de atualizar.
Versão 3.0.0 porque o rigor é a correção — se você alimentava esta biblioteca com strings não canônicas, a v2 estava lhe dando respostas erradas, não compatíveis:
isPrivate / isPublic / isLoopback / toBuffer / toLong lançam exceção em entrada malformada ou não canônica (octetos octais/hexadecimais, 127.1, strings de inteiros). Use normalizeToLong() primeiro se precisar aceitar notações legadas.isLoopback('fe80::1') agora é false (link-local ≠ loopback; ainda é isPrivate). isLoopback('::') agora é false (não especificado ≠ loopback; ainda é isPrivate).loopback('ipv6') retorna '::1' em vez de 'fe80::1'.isPrivate() retorna true para mais faixas (CGNAT, TEST-NETs, multicast, …) — agora significa "não globalmente acessível".address('public') agora realmente retorna endereços públicos. Se você dependia do comportamento invertido para obter um endereço de LAN, use address('private') — era isso que ele estava lhe dando.Tudo o que o original exporta, mais 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'