diff --git a/lib/ip.js b/lib/ip.js index 9022443..1553db7 100644 --- a/lib/ip.js +++ b/lib/ip.js @@ -338,18 +338,29 @@ ip.isPublic = function (addr) { }; ip.isLoopback = function (addr) { - // If addr is an IPv4 address in long integer form (no dots and no colons), convert it - if (!/\./.test(addr) && !/:/.test(addr)) { - addr = ip.fromLong(Number(addr)); + // Check IPv6 loopback addresses first + if (/:/.test(addr)) { + return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/.test(addr) + || /^fe80::1$/i.test(addr) + || /^::1$/.test(addr) + || /^::$/.test(addr); } - return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/ - .test(addr) - || /^0177\./.test(addr) - || /^0x7f\./i.test(addr) - || /^fe80::1$/i.test(addr) - || /^::1$/.test(addr) - || /^::$/.test(addr); + // For IPv4 addresses, normalize to handle all formats: + // - Standard: 127.0.0.1 + // - Shorthand: 127.1, 127.0.1 + // - Octal: 0177.0.0.1, 0177.1 + // - Hex: 0x7f.0.0.1, 0x7f.1 + // - Long integer: 2130706433 + const ipLong = ip.normalizeToLong(addr); + if (ipLong < 0) { + return false; + } + + // Loopback range is 127.0.0.0/8 (127.0.0.0 - 127.255.255.255) + // In long format: 2130706432 (127.0.0.0) to 2147483647 (127.255.255.255) + // First octet check: (ipLong >>> 24) === 127 + return (ipLong >>> 24) === 127; }; ip.loopback = function (family) { diff --git a/test/api-test.js b/test/api-test.js index 0db838d..63b1d84 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -506,4 +506,41 @@ describe('IP library for node.js', () => { it('should return false for "192.168.1.1"', () => { assert.equal(ip.isLoopback('192.168.1.1'), false); }); + + // CVE-2024-29415: SSRF bypass via IPv4 shorthand notation + describe('CVE-2024-29415 - IPv4 shorthand loopback detection', () => { + it('should detect "127.1" as loopback (shorthand for 127.0.0.1)', () => { + assert.equal(ip.isLoopback('127.1'), true); + }); + + it('should detect "127.0.1" as loopback (shorthand for 127.0.0.1)', () => { + assert.equal(ip.isLoopback('127.0.1'), true); + }); + + it('should detect "127.1" as private', () => { + assert.equal(ip.isPrivate('127.1'), true); + }); + + it('should detect "127.0.1" as private', () => { + assert.equal(ip.isPrivate('127.0.1'), true); + }); + + // Ensure non-loopback shorthand is not falsely detected + it('should not detect "192.1" as loopback', () => { + assert.equal(ip.isLoopback('192.1'), false); + }); + + it('should not detect "10.1" as loopback', () => { + assert.equal(ip.isLoopback('10.1'), false); + }); + + // Edge cases at loopback range boundaries + it('should not detect "126.255.255.255" as loopback', () => { + assert.equal(ip.isLoopback('126.255.255.255'), false); + }); + + it('should not detect "128.0.0.0" as loopback', () => { + assert.equal(ip.isLoopback('128.0.0.0'), false); + }); + }); });