ip(原仓库)的维护分支——Node.js 的 IP 地址工具库——修复了未修补的 SSRF 安全公告。
原包每月约 3800 万次下载,自 2024 年 2 月以来未发布新版本,并且有一个影响每个已发布版本的高严重性安全公告(CVE-2024-29415 / GHSA-2p57-rm9w-gvfp),导致每次安装时 npm audit 失败。本分支修复了根本原因,而非修补正则表达式。
npm install @bybrave/ip2
// CommonJS — 可直接替换
const ip = require('@bybrave/ip2');
// ESM — 具名导入
import { isPrivate, isPublic, cidrSubnet } from '@bybrave/ip2';
// 内置 TypeScript 类型
原始的 isPrivate() / isPublic() / isLoopback() 对原始输入字符串运行正则表达式。任何正则表达式未预见的地址拼写方式(八进制或十六进制字节、缩写 IPv4、未压缩 IPv6)都会被静默归类为公网地址:
// 原始 [email protected] — 以下全是回环/私有地址,但全部报告为公网:
ip.isPublic('127.1') // true 💥
ip.isPublic('012.1.2.3') // true 💥 (八进制 10.1.2.3)
ip.isPublic('01200034567') // true 💥 (整数 10.0.14.119)
ip.isPublic('000:0:0000::01') // true 💥 (即 ::1)
如果这些判断结果用于保护出站请求,就构成了 SSRF 漏洞(CVE-2024-29415,CVE-2023-42282 的不完全修复)。
本分支:
net.isIP 相同的语法,外加 IPv6 区域 ID)将地址解析为字节。分类基于字节进行,绝不基于字符串。127.1、0x7f.1、0177.0.0.1、2130706433)抛出异常而非猜测。失败关闭而非开放。如果需要接受遗留表示法,请先显式标准化:ip.fromLong(ip.normalizeToLong(addr))。isPrivate() 现在表示“全局不可达”——与 Python 的 ipaddress 约定相同。这新增了 CGNAT(100.64/10)、TEST-NET、基准测试、组播、保留、ULA、文档范围等——见下方表格。::ffff:a.b.c.d(v4 映射)、64:ff9b::a.b.c.d(NAT64)和 2002:xxxx:xxxx::(6to4)根据其内部的 IPv4 地址判断私有/公网。// @bybrave/ip2
ip.isPublic('127.1') // 抛出 Error: Invalid ip address: 127.1
ip.isPrivate('000:0:0000::01') // true (解析为 ::1)
ip.isLoopback('::fFFf:127.0.0.1')// true
ip.isPublic('64:ff9b::8.8.8.8') // true (公网地址的 NAT64)
ip.isPrivate('100.64.0.1') // true (CGNAT——原版报告为公网)
[email protected] 的所有修复- const ip = require('ip');
+ const ip = require('@bybrave/ip2');
API 表面相同;行为差异在于安全性和正确性修复——升级前请查看下方的破坏性变更。
版本号 3.0.0 是因为严格性本身就是修复——如果您向此库提供非规范字符串,v2 会给出错误答案,而非兼容的答案:
isPrivate / isPublic / isLoopback / toBuffer / toLong 在遇到格式错误或非规范输入(八进制/十六进制字节、127.1、整数字符串)时抛出异常。如果必须接受遗留表示法,请先使用 normalizeToLong()。isLoopback('fe80::1') 现在返回 false(链路本地 ≠ 回环;但仍是 isPrivate)。isLoopback('::') 现在返回 false(未指定 ≠ 回环;但仍是 isPrivate)。loopback('ipv6') 返回 '::1' 而非 。原始包导出的所有内容,外加 isValid():
ip.address('public', 'ipv6') // 网络接口的地址
ip.isPrivate('10.0.0.1') // true — 全局不可达
ip.isPublic('8.8.8.8') // true
ip.isLoopback('127.8.8.8') // true
ip.isValid('300.1.2.3') // false(从不抛出异常)
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(宽松模式,出错返回 -1)
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 映射)
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(包括广播)。
IPv6: ::/96(未指定、回环、v4 兼容)、64:ff9b:1::/48、100::/64、2001::/23、2001:db8::/32、3fff::/20、5f00::/16、fc00::/7、fe80::/10、fec0::/10、ff00::/8。过渡范围 ::ffff:0:0/96、64:ff9b::/96 和 2002::/16 遵循其嵌入的 IPv4 地址。
如果此包为您节省了时间,您可以支持维护工作:
比特币(BTC):bc1q37557q5jpeaxqydzwvf3jgj7zhnfpn2td3q40q
MIT。基于 Fedor Indutny 的 node-ip。
| 修复内容 | 原始问题 |
|---|
| CVE-2024-29415 / GHSA-2p57-rm9w-gvfp — 通过非规范地址的 SSRF | #136、#150、#153、#158 |
isPrivate() 遗漏了 CGNAT、TEST-NET、基准测试、组播、保留、文档范围 | — |
cidrSubnet(...).contains() 对 IPv4 映射的 IPv6 地址返回 false | #104 |
isV6Format() 对 IPv4 地址返回 true | #67 |
isV4Format() 接受 ≥ 256 的字节 | #105 |
address('public') 返回了私有地址,反之亦然(过滤器反转) | — |
loopback('ipv6') 返回 fe80::1,这是链路本地地址,不是回环地址 → 现在返回 ::1 | — |
cidr()/cidrSubnet() 对无效前缀长度(/33、/x)静默产生垃圾结果 → 现在抛出异常 | — |
address(name) 对未知接口名称抛出 TypeError → 现在返回 undefined | — |
内置 TypeScript 类型(无需 @types/ip)、ESM 具名导出 | — |
'fe80::1'isPrivate() 对更多范围返回 true(CGNAT、TEST-NET、组播等)——现在它表示“全局不可达”。address('public') 现在实际上返回公网地址。如果您依赖反转行为来获取局域网地址,请使用 address('private')——那正是之前它返回给您的。