
فرع مُحافظ عليه من node-ip مع إصلاح الاستشارة غير المُصححة لـ SSRF (CVE-2024-29415)
نسخة مُصانة (fork) من ip — أدوات عناوين IP لـ Node.js — مع إصلاح ثغرة SSRF غير المُرقَّعة.
الحزمة الأصلية لديها ~38 مليون تنزيل شهريًا، ولا يوجد إصدار منذ فبراير 2024، ولديها تنبيه أمني بدرجة HIGH (CVE-2024-29415 / GHSA-2p57-rm9w-gvfp) يؤثر على كل إصدار منشور ويجعل npm audit يفشل عند كل تثبيت. هذه النسخة المُصانة تصلح السبب الجذري بدلًا من ترقيع التعبيرات النمطية (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
الدوال الأصلية isPrivate() / isPublic() / isLoopback() كانت تُشغّل تعبيرات نمطية على سلسلة الإدخال الخام. أي كتابة لعنوان لم تتوقعها التعبيرات النمطية — بايتات ثمانية أو سادس عشرية، IPv4 مختصر، IPv6 غير مضغوط — كانت تُصنَّف بصمت على أنها عامة:
// 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). يجري التصنيف على البايتات، وليس على السلاسل النصية أبدًا.127.1، 0x7f.1، 0177.0.0.1، 2130706433) تُطلق خطأً بدلًا من التخمين. الفشل مغلق، وليس مفتوحًا. إذا كنت بحاجة لقبول الصيغ القديمة، طبّعها صراحةً أولًا: ip.fromLong(ip.normalizeToLong(addr)).isPrivate() يعني "غير قابل للوصول عالميًا" — نفس اصطلاح ipaddress في Python. يضيف ذلك CGNAT (100.64/10)، وTEST-NETs، والقياس المرجعي (benchmarking)، والبث المتعدد (multicast)، والمحجوز (reserved)، وULA، ونطاقات التوثيق والمزيد — انظر الجدول أدناه.::ffff:a.b.c.d (v4-mapped)، و64:ff9b::a.b.c.d (NAT64)، و2002:xxxx:xxxx:: (6to4) تُعدّ خاصة/عامة وفقًا لعنوان IPv4 الموجود داخلها.// @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-NETs، والقياس المرجعي، والبث المتعدد، والمحجوز، ونطاقات التوثيق | — |
cidrSubnet(...).contains() تُعيد false لعناوين IPv6 المُعبَّر عنها كـ IPv4-mapped | #104 |
isV6Format() تُعيد true لعناوين IPv4 | #67 |
isV4Format() تقبل بايتات ≥ 256 | #105 |
address('public') كانت تُعيد عناوين خاصة والعكس صحيح (المرشّح كان معكوسًا) | — |
loopback('ipv6') كانت تُعيد fe80::1، وهو link-local وليس loopback → الآن ::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 (link-local ≠ loopback؛ لكنها تبقى isPrivate). isLoopback('::') أصبحت الآن false (غير محدد ≠ loopback؛ لكنها تبقى isPrivate).loopback('ipv6') تُعيد '::1' بدلًا من 'fe80::1'.isPrivate() تُعيد true لنطاقات أكثر (CGNAT، TEST-NETs، البث المتعدد، …) — أصبحت تعني "غير قابل للوصول عالميًا".address('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)