
Node.js向けIPアドレスユーティリティ ip のメンテナンスフォークで、未パッチのSSRFアドバイザリを修正済みです。
元パッケージは月間約3,800万ダウンロードがありますが、2024年2月以降リリースがなく、公開されたすべてのバージョンに影響する深刻度HIGHのアドバイザリ(CVE-2024-29415 / GHSA-2p57-rm9w-gvfp)が存在し、npm audit がすべてのインストールで失敗します。このフォークは正規表現にパッチを当てるのではなく、根本原因を修正します。
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)は推測する代わりに例外をスローします。フェイルクローズであり、フェイルオープンではありません。レガシーな表記を受け入れる必要がある場合は、明示的に正規化してください: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)、(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] に対するすべての修正- const ip = require('ip');
+ const ip = require('@bybrave/ip2');
APIサーフェスは同じです。動作の違いはセキュリティと正確性の修正によるものです。アップグレード前に以下の破壊的変更を確認してください。
厳格化こそが修正であるため、バージョン3.0.0としました。このライブラリに非標準的な文字列を渡した場合、v2は互換性のある答えではなく誤った答えを返していました:
isPrivate / isPublic / isLoopback / toBuffer / toLong は、不正または非標準的な入力(8進数・16進数のオクテット、127.1、整数文字列)に対して例外をスローします。レガシーな表記を受け入れる必要がある場合は、最初に normalizeToLong() を使用してください。isLoopback('fe80::1') は現在 false です(リンクローカル ≠ ループバック。isPrivate では引き続き true)。isLoopback('::') は現在 false です(未指定 ≠ ループバック。isPrivate では引き続き true)。元のエクスポートに加え、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)
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ライセンス。node-ip(Fedor Indutny氏作)に基づいています。
2002:xxxx:xxxx::| 修正内容 | 元の問題 |
|---|
| 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') が private アドレスを返し、その逆も同様だった(フィルタが反転していた) | — |
loopback('ipv6') がリンクローカルであってループバックではない fe80::1 を返していた → 現在は ::1 | — |
cidr()/cidrSubnet() が無効なプレフィックス長(/33、/x)で静かに壊れた結果を生成していた → 現在は例外をスロー | — |
address(name) が不明なインターフェース名で TypeError を発生させていた → 現在は undefined を返す | — |
TypeScript型を内蔵(@types/ip 不要)、ESM名前付きエクスポート | — |
loopback('ipv6') は 'fe80::1' の代わりに '::1' を返します。isPrivate() はより多くのレンジ(CGNAT、TEST-NET、マルチキャストなど)に対して true を返します。「グローバルに到達不能」という意味になりました。address('public') は実際にパブリックアドレスを返すようになりました。逆転した動作に依存してLANアドレスを取得していた場合は、address('private') を使用してください。以前はそれが返されていました。