@bybrave/ip2

中文文档
3.1.0发布于 2026/10/14MITTypeScriptESM + CJS零依赖近 30 天下载 5472

Maintained fork of ip — IP address utilities with the unpatched SSRF advisory (CVE-2024-29415 / GHSA-2p57-rm9w-gvfp) fixed

原始使用文档
文档目录10 节
  1. The vulnerability, and how it is fixed
  2. All fixes over [email protected]
  3. Migrating from ip
  4. Breaking changes (v3)
  5. API
  6. Invalid input throws
  7. Browsers and bundlers
  8. Ranges treated as private (not globally reachable)
  9. Support
  10. Credits & license

@bybrave/ip2

CI npm

Maintained fork of ip — IP address utilities for Node.js — with the unpatched SSRF advisory fixed.

The original package has ~38M downloads/month, no release since February 2024, and a HIGH-severity advisory (CVE-2024-29415 / GHSA-2p57-rm9w-gvfp) that affects every published version and makes npm audit fail on every install. This fork fixes the root cause instead of patching 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

The vulnerability, and how it is fixed

The original isPrivate() / isPublic() / isLoopback() ran regexes over the raw input string. Any spelling of an address the regexes didn't anticipate — octal or hex octets, abbreviated IPv4, uncompressed IPv6 — was silently classified as public:

// 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)

If those answers guard outgoing requests, that's an SSRF hole (CVE-2024-29415, incomplete fix of CVE-2023-42282).

This fork:

  1. Parses first, classifies second. Every classification function parses the address into bytes with a strict parser (same grammar as net.isIP, plus IPv6 zone IDs). Classification runs on bytes, never on strings.
  2. Rejects ambiguous input. Non-canonical spellings (127.1, 0x7f.1, 0177.0.0.1, 2130706433) throw instead of being guessed at. Fail closed, not open. If you need to accept legacy notations, normalize explicitly first: ip.fromLong(ip.normalizeToLong(addr)).
  3. Covers the full IANA special-purpose registries (IPv4, IPv6), not four regexes. isPrivate() now means "not globally reachable" — the same convention as Python's ipaddress. That adds CGNAT (100.64/10), TEST-NETs, benchmarking, multicast, reserved, ULA, documentation ranges and more — see the table below.
  4. Classifies transition addresses by their embedded IPv4. ::ffff:a.b.c.d (v4-mapped), 64:ff9b::a.b.c.d (NAT64) and 2002:xxxx:xxxx:: (6to4) are private/public according to the IPv4 address inside them.
// @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)

All fixes over [email protected]

FixedOriginal issue
CVE-2024-29415 / GHSA-2p57-rm9w-gvfp — SSRF via non-canonical addresses#136, #150, #153, #158
isPrivate() misses CGNAT, TEST-NETs, benchmarking, multicast, reserved, documentation ranges—
cidrSubnet(...).contains() returns false for IPv4-mapped IPv6 addresses#104
isV6Format() returns true for IPv4 addresses#67
isV4Format() accepts octets ≥ 256#105
address('public') returned private addresses and vice versa (filter was inverted)—
loopback('ipv6') returned fe80::1, which is link-local, not loopback → now ::1—
cidr()/cidrSubnet() silently produced garbage for invalid prefix lengths (/33, /x) → now throw—
address(name) crashed with TypeError for unknown interface names → now returns undefined—
cidrSubnet()/subnet() gave wrong answers for IPv6 (/48 contained a neighbouring /48) → real IPv6 subnet math#89, #110
Bundling for the browser failed on os (webpack 5) → browser field maps it away#127, #132
TypeScript types built in (no @types/ip needed), ESM named exports—

Migrating from ip

- const ip = require('ip');
+ const ip = require('@bybrave/ip2');

The API surface is the same; the behavioural differences are the security and correctness fixes — see the breaking changes below before upgrading.

Breaking changes (v3)

Version 3.0.0 because strictness is the fix — if you fed this library non-canonical strings, v2 was giving you wrong answers, not compatible ones:

  • isPrivate / isPublic / isLoopback / toBuffer / toLong throw on malformed or non-canonical input (octal/hex octets, 127.1, integer strings). Use normalizeToLong() first if you must accept legacy notations.
  • isLoopback('fe80::1') is now false (link-local ≠ loopback; still isPrivate). isLoopback('::') is now false (unspecified ≠ loopback; still isPrivate).
  • loopback('ipv6') returns '::1' instead of 'fe80::1'.
  • isPrivate() returns true for more ranges (CGNAT, TEST-NETs, multicast, …) — it now means "not globally reachable".
  • address('public') actually returns public addresses now. If you relied on the inverted behaviour to get a LAN address, use address('private') — that's what it was giving you.
  • Node.js ≥ 18.

API

Everything the original exports, plus 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)

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 (approximate above 2 ** 53)

IPv6 subnets have no broadcast or reserved host addresses: firstAddress is the network address and broadcastAddress equals lastAddress. ip.subnet(addr, mask) uses IPv6 math when both the address and the mask are IPv6. Plain IPv4 addresses are matched by IPv6 subnets through the IPv4-mapped range (::ffff:0:0/96).

Invalid input throws

isPrivate, isPublic, isLoopback, toBuffer and toLong throw on malformed input instead of returning a guess (#146). When the input is untrusted, check it first or catch:

const isInternal = ip.isValid(host) && ip.isPrivate(host)

Browsers and bundlers

The library is Node-first. In a bundle, os is mapped away through the browser field, so address() is unavailable, and everything else works once Buffer is available. With webpack 5, add the polyfill (#127, #132):

// webpack.config.js, after `npm i buffer`
resolve: { fallback: { buffer: require.resolve('buffer/') } }

Ranges treated as private (not globally reachable)

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 (unspecified, loopback, v4-compatible), 64:ff9b:1::/48, 100::/64, 2001::/23, 2001:db8::/32, 3fff::/20, 5f00::/16, fc00::/7, fe80::/10, fec0::/10, ff00::/8. Transition ranges ::ffff:0:0/96, 64:ff9b::/96 and 2002::/16 follow their embedded IPv4 address.

Support

If this package saves you time, you can support maintenance:

Ko-fi Bitcoin

Bitcoin (BTC): bc1q37557q5jpeaxqydzwvf3jgj7zhnfpn2td3q40q

Credits & license

MIT. Based on node-ip by Fedor Indutny.

版本与发布记录

@bybrave/ip2 共发布 2 个版本,首次发布于 2026/07/05;过去 12 个月发布了 2 个版本。

版本发布日期距今
3.1.0latest2026/10/1425 天前
3.0.02026/07/054 个月前

关于 @bybrave/ip2 的常见问题

@bybrave/ip2 是什么?

Maintained fork of ip — IP address utilities with the unpatched SSRF advisory (CVE-2024-29415 / GHSA-2p57-rm9w-gvfp) fixed

如何安装 @bybrave/ip2?

在项目目录运行 npm install @bybrave/ip2,或使用 pnpm add / yarn add / bun add。

@bybrave/ip2 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bybrave/ip2 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@bybrave/ip2 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@bybrave/ip2 可以商用吗?

@bybrave/ip2 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@bybrave/ip2 的下载量有多少?

根据 npm 官方下载统计,@bybrave/ip2 在 2026/10/09 – 2026/11/07 这 30 天内被下载 5,472 次,日均约 182 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bybrave/ip2 还在维护吗?

最新版本 3.1.0 发布于 2026/10/14(25 天前);至今共发布 2 个版本,其中过去 12 个月发布了 2 个;首次发布于 2026/07/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。