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] — 以下全部是环回/私有地址,却全部报告为 PUBLIC:
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] 的全部修复| 已修复 | 原始问题 |
|---|---|
| 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 | — |
cidrSubnet()/subnet() 对 IPv6 给出错误结果(/48 包含了相邻的 /48)→ 真正的 IPv6 子网计算 | #89、#110 |
浏览器打包在 os 上失败(webpack 5)→ browser 字段将其映射掉 | #127、#132 |
内置 TypeScript 类型(无需 @types/ip),ESM 命名导出 | — |
- 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' 而不是 'fe80::1'。isPrivate() 对更多范围返回 true(CGNAT、TEST-NET、组播……)——它现在表示“非全局可达”。address('public') 现在实际返回公网地址。如果你依赖反转行为来获取 LAN 地址,请使用 address('private')——那才是它之前给你的结果。原始版本导出的所有内容,外加 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 映射)
const s6 = ip.cidrSubnet('2001:db8:f53b::/48')
s6.lastAddress // '2001:db8:f53b:ffff:ffff:ffff:ffff:ffff'
s6.contains('2001:db8:f53a::1') // false
s6.length // 2 ** 80(超过 2 ** 53 时近似)
IPv6 子网没有广播地址或保留主机地址:firstAddress 是网络地址,broadcastAddress 等于 lastAddress。当地址和掩码都是 IPv6 时,ip.subnet(addr, mask) 使用 IPv6 计算。纯 IPv4 地址通过 IPv4 映射范围(::ffff:0:0/96)被 IPv6 子网匹配。
isPrivate、isPublic、isLoopback、toBuffer 和 toLong 对格式错误的输入抛出异常,而不是返回猜测结果(#146)。当输入不可信时,请先检查或捕获:
const isInternal = ip.isValid(host) && ip.isPrivate(host)
该库以 Node 优先。在打包环境中,os 通过 browser 字段被映射掉,因此 address() 不可用,其余功能在 Buffer 可用后即可正常工作。使用 webpack 5 时,添加 polyfill(#127、#132):
// webpack.config.js,在 `npm i buffer` 之后
resolve: { fallback: { buffer: require.resolve('buffer/') } }
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 地址。
如果此包为你节省了时间,你可以支持维护工作:
Bitcoin (BTC):bc1q37557q5jpeaxqydzwvf3jgj7zhnfpn2td3q40q
MIT。基于 Fedor Indutny 的 node-ip。