From 6a3ada9b471b09d5f0f5be264911ab564bf67894 Mon Sep 17 00:00:00 2001 From: Luke McFarlane Date: Mon, 12 Feb 2024 13:22:18 +1100 Subject: [PATCH] lib: fixed CVE-2023-42282 and added unit test --- lib/ip.js | 77 ++++++++++++++++++++++++++++++++++++--- test/api-test.js | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 4 deletions(-) diff --git a/lib/ip.js b/lib/ip.js index 5b5ccc2..3f6a522 100644 --- a/lib/ip.js +++ b/lib/ip.js @@ -311,12 +311,26 @@ ip.isEqual = function (a, b) { }; ip.isPrivate = function (addr) { - return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i - .test(addr) + // check loopback addresses first + if (ip.isLoopback(addr)) { + return true; + } + + // ensure the ipv4 address is valid + if (!ip.isV6Format(addr)) { + const ipl = ip.normalizeToLong(addr); + if (ipl < 0) { + throw new Error('invalid ipv4 address'); + } + // normalize the address for the private range checks that follow + addr = ip.fromLong(ipl); + } + + // check private ranges + return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i .test(addr) - || /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) || /^f[cd][0-9a-f]{2}:/i.test(addr) || /^fe80:/i.test(addr) @@ -329,9 +343,16 @@ 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)); + } + return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/ .test(addr) - || /^fe80::1$/.test(addr) + || /^0177\./.test(addr) + || /^0x7f\./i.test(addr) + || /^fe80::1$/i.test(addr) || /^::1$/.test(addr) || /^::$/.test(addr); }; @@ -425,3 +446,51 @@ ip.fromLong = function (ipl) { ipl >> 8 & 255}.${ ipl & 255}`); }; + +ip.normalizeToLong = function (addr) { + const parts = addr.split('.').map(part => { + // Handle hexadecimal format + if (part.startsWith('0x') || part.startsWith('0X')) { + return parseInt(part, 16); + } + // Handle octal format (strictly digits 0-7 after a leading zero) + else if (part.startsWith('0') && part !== '0' && /^[0-7]+$/.test(part)) { + return parseInt(part, 8); + } + // Handle decimal format, reject invalid leading zeros + else if (/^[1-9]\d*$/.test(part) || part === '0') { + return parseInt(part, 10); + } + // Return NaN for invalid formats to indicate parsing failure + else { + return NaN; + } + }); + + if (parts.some(isNaN)) return -1; // Indicate error with -1 + + let val = 0; + const n = parts.length; + + switch (n) { + case 1: + val = parts[0]; + break; + case 2: + if (parts[0] > 0xff || parts[1] > 0xffffff) return -1; + val = (parts[0] << 24) | (parts[1] & 0xffffff); + break; + case 3: + if (parts[0] > 0xff || parts[1] > 0xff || parts[2] > 0xffff) return -1; + val = (parts[0] << 24) | (parts[1] << 16) | (parts[2] & 0xffff); + break; + case 4: + if (parts.some(part => part > 0xff)) return -1; + val = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; + break; + default: + return -1; // Error case + } + + return val >>> 0; +}; diff --git a/test/api-test.js b/test/api-test.js index f0fd222..0db838d 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -251,6 +251,57 @@ describe('IP library for node.js', () => { }); }); + describe('normalizeIpv4() method', () => { + // Testing valid inputs with different notations + it('should correctly normalize "127.0.0.1"', () => { + assert.equal(ip.normalizeToLong('127.0.0.1'), 2130706433); + }); + + it('should correctly handle "127.1" as two parts', () => { + assert.equal(ip.normalizeToLong('127.1'), 2130706433); + }); + + it('should correctly handle "127.0.1" as three parts', () => { + assert.equal(ip.normalizeToLong('127.0.1'), 2130706433); + }); + + + it('should correctly handle hexadecimal notation "0x7f.0x0.0x0.0x1"', () => { + assert.equal(ip.normalizeToLong('0x7f.0x0.0x0.0x1'), 2130706433); + }); + + // Testing with fewer than 4 parts + it('should correctly handle "0x7f000001" as a single part', () => { + assert.equal(ip.normalizeToLong('0x7f000001'), 2130706433); + }); + + it('should correctly handle octal notation "010.0.0.01"', () => { + assert.equal(ip.normalizeToLong('010.0.0.01'), 134217729); + }); + + // Testing invalid inputs + it('should return -1 for an invalid address "256.100.50.25"', () => { + assert.equal(ip.normalizeToLong('256.100.50.25'), -1); + }); + + it('should return -1 for an address with invalid octal "019.0.0.1"', () => { + assert.equal(ip.normalizeToLong('019.0.0.1'), -1); + }); + + it('should return -1 for an address with invalid hex "0xGG.0.0.1"', () => { + assert.equal(ip.normalizeToLong('0xGG.0.0.1'), -1); + }); + + // Testing edge cases + it('should return -1 for an empty string', () => { + assert.equal(ip.normalizeToLong(''), -1); + }); + + it('should return -1 for a string with too many parts "192.168.0.1.100"', () => { + assert.equal(ip.normalizeToLong('192.168.0.1.100'), -1); + }); + }); + describe('isPrivate() method', () => { it('should check if an address is localhost', () => { assert.equal(ip.isPrivate('127.0.0.1'), true); @@ -300,6 +351,10 @@ describe('IP library for node.js', () => { assert.equal(ip.isPrivate('::1'), true); assert.equal(ip.isPrivate('fe80::1'), true); }); + + it('should correctly identify hexadecimal IP addresses like \'0x7f.1\' as private', () => { + assert.equal(ip.isPrivate('0x7f.1'), true); + }); }); describe('loopback() method', () => { @@ -413,4 +468,42 @@ describe('IP library for node.js', () => { assert.equal(ip.fromLong(4294967295), '255.255.255.255'); }); }); + + // IPv4 loopback in octal notation + it('should return true for octal representation "0177.0.0.1"', () => { + assert.equal(ip.isLoopback('0177.0.0.1'), true); + }); + + it('should return true for octal representation "0177.0.1"', () => { + assert.equal(ip.isLoopback('0177.0.1'), true); + }); + + it('should return true for octal representation "0177.1"', () => { + assert.equal(ip.isLoopback('0177.1'), true); + }); + + // IPv4 loopback in hexadecimal notation + it('should return true for hexadecimal representation "0x7f.0.0.1"', () => { + assert.equal(ip.isLoopback('0x7f.0.0.1'), true); + }); + + // IPv4 loopback in hexadecimal notation + it('should return true for hexadecimal representation "0x7f.0.1"', () => { + assert.equal(ip.isLoopback('0x7f.0.1'), true); + }); + + // IPv4 loopback in hexadecimal notation + it('should return true for hexadecimal representation "0x7f.1"', () => { + assert.equal(ip.isLoopback('0x7f.1'), true); + }); + + // IPv4 loopback as a single long integer + it('should return true for single long integer representation "2130706433"', () => { + assert.equal(ip.isLoopback('2130706433'), true); + }); + + // IPv4 non-loopback address + it('should return false for "192.168.1.1"', () => { + assert.equal(ip.isLoopback('192.168.1.1'), false); + }); });