
node-ip의 유지 관리 포크로, 패치되지 않은 SSRF 권고(CVE-2024-29415)가 수정되었습니다.
ip — Node.js용 IP 주소 유틸리티 — 의 유지 관리되는 포크로, 패치되지 않은 SSRF 취약점이 수정되었습니다.
원본 패키지는 월 약 3,800만 다운로드를 기록하지만 2024년 2월 이후 릴리스가 없으며, 모든 게시된 버전에 영향을 미치고 모든 설치 시 npm audit을 실패하게 만드는 HIGH 심각도 권고(CVE-2024-29415 / GHSA-2p57-rm9w-gvfp)가 있습니다. 이 포크는 정규식을 패치하는 대신 근본 원인을 수정합니다.
npm install @bybrave/ip2
// CommonJS — 드롭인
const ip = require('@bybrave/ip2');
// ESM — 명명된 가져오기
import { isPrivate, isPublic, cidrSubnet } from '@bybrave/ip2';
// TypeScript 타입 내장
원본 isPrivate() / isPublic() / isLoopback()은 원시 입력 문자열에 대해 정규식을 실행했습니다. 정규식이 예상하지 못한 주소 표기법 — 8진수 또는 16진수 옥텟, 축약형 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 존 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')은 이제 실제로 공용 주소를 반환합니다. 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/') } }