
ip(Node.js 向け IP アドレスユーティリティ)のメンテナンスされたフォークです。未修正だった SSRF アドバイザリを修正しています。
オリジナルのパッケージは月間約 3,800 万ダウンロード、2024 年 2 月以降リリースがなく、公開されているすべてのバージョンに影響し、インストールのたびに npm audit を失敗させる HIGH 深刻度のアドバイザリ(CVE-2024-29415 / GHSA-2p57-rm9w-gvfp)があります。このフォークは正規表現をパッチするのではなく、根本原因を修正します。
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
オリジナルの isPrivate() / isPublic() / isLoopback() は、生の入力文字列に対して正規表現を実行していました。正規表現が想定していないアドレスの表記 — 8 進数や 16 進数のオクテット、省略形 IPv4、非圧縮 IPv6 — は、黙って 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)
これらの結果が送信リクエストをガードしている場合、それは SSRF の穴です(CVE-2024-29415、CVE-2023-42282 の不完全な修正)。
このフォーク:
net.isIP と同じ文法、加えて IPv6 ゾーン ID)でアドレスをバイトにパースします。分類はバイトに対して実行され、文字列に対しては決して実行されません。127.1、0x7f.1、0177.0.0.1、2130706433)は推測されるのではなく throw されます。フェイルクローズドであり、フェイルオープンではありません。レガシー表記を受け入れる必要がある場合は、まず明示的に正規化してください: ip.fromLong(ip.normalizeToLong(addr))。isPrivate() は「グローバルに到達可能でない」という意味になりました — Python の ipaddress と同じ規約です。これにより CGNAT(100.64/10)、TEST-NET、ベンチマーク、マルチキャスト、予約済み、ULA、ドキュメンテーション範囲などが追加されます — 以下の表を参照してください。::ffff:a.b.c.d(v4-mapped)、64:ff9b::a.b.c.d(NAT64)、2002:xxxx:xxxx::(6to4)は、内部の IPv4 アドレスに従って private/public となります。// @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] に対するすべての修正| 修正内容 | 元の問題 |
|---|---|
| CVE-2024-29415 / GHSA-2p57-rm9w-gvfp — 非正規アドレス経由の SSRF | #136, #150, #153, #158 |
isPrivate() が CGNAT、TEST-NET、ベンチマーク、マルチキャスト、予約済み、ドキュメンテーション範囲を見逃す | — |
cidrSubnet(...).contains() が IPv4-mapped IPv6 アドレスに対して false を返す | #104 |
isV6Format() が IPv4 アドレスに対して true を返す | #67 |
isV4Format() が 256 以上のオクテットを受け入れる | #105 |
address('public') が private アドレスを返し、その逆も同様(フィルタが反転していた) | — |
loopback('ipv6') が fe80::1 を返していた。これはリンクローカルであり、ループバックではない → 現在は ::1 | — |
cidr()/cidrSubnet() が無効なプレフィックス長(/33、/x)に対して黙ってゴミを生成 → 現在は throw | — |
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 は、不正または非正規の入力(8 進数/16 進数のオクテット、127.1、整数文字列)に対して throw します。レガシー表記を受け入れる必要がある場合は、まず normalizeToLong() を使用してください。isLoopback('fe80::1') は現在 false です(リンクローカル ≠ ループバック。ただし isPrivate のまま)。isLoopback('::') は現在 false です(未指定 ≠ ループバック。ただし isPrivate のまま)。loopback('ipv6') は 'fe80::1' ではなく '::1' を返します。isPrivate() はより多くの範囲(CGNAT、TEST-NET、マルチキャストなど)に対して true を返します — これは「グローバルに到達可能でない」という意味になりました。address('public') は実際に public アドレスを返すようになりました。LAN アドレスを取得するために反転した動作に依存していた場合は、address('private') を使用してください — それが以前に得られていたものです。オリジナルがエクスポートするすべてに加えて 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 サブネットにはブロードキャストアドレスや予約済みホストアドレスはありません: firstAddress はネットワークアドレスであり、broadcastAddress は lastAddress と等しくなります。ip.subnet(addr, mask) は、アドレスとマスクの両方が IPv6 の場合に IPv6 演算を使用します。プレーンな IPv4 アドレスは、IPv4-mapped 範囲(::ffff:0:0/96)を通じて IPv6 サブネットにマッチします。
isPrivate、isPublic、isLoopback、toBuffer、toLong は、推測を返す代わりに不正な入力に対して throw します(#146)。入力が信頼できない場合は、まずチェックするか catch してください:
const isInternal = ip.isValid(host) && ip.isPrivate(host)
このライブラリは Node ファーストです。バンドル内では、os は browser フィールドを通じてマップされて回避されるため、address() は利用できません。それ以外は Buffer が利用可能であればすべて動作します。webpack 5 では、ポリフィルを追加してください(#127、#132):
// webpack.config.js, after `npm i buffer`
resolve: { fallback: { buffer: require.resolve('buffer/') } }