CVE-2023-42282 (published 2024-02-08T17:15Z)

The ip package before 1.1.9 for Node.js might allow SSRF because some IP addresses (such as 0x7f.1) are improperly categorized as globally routable via isPublic.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/indutny/node-ip
commit_details: repo_url: https://github.com/indutny/node-ip
author: Luke McFarlane
author_date: 2024-02-12 13:22:18+11:00
msg: lib: fixed CVE-2023-42282 and added unit test
repository: repo_url: https://github.com/indutny/node-ip
repo_name: indutny/node-ip
description: IP address tools for node.js
repo_language: JavaScript
owner: indutny
file_changes: filename: ip.js
change_type: ModificationType.MODIFY
diff: @@ -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;
+};

programming_language: JavaScript
filename: api-test.js
change_type: ModificationType.MODIFY
diff: @@ -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);
+  });
 });

programming_language: TypeScript