CVE-2023-42282 (JavaScript, published 2024-02-08T17:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
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.
Repository: https://github.com/indutny/node-ip
Files changed:
lib/ip.js
test/api-test.js
Commit message: lib: fixed CVE-2023-42282 and added unit test


Vulnerable code:
  return /^(::f{4}:)?10\.([0-9]{1,3})\.([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)
    || /^fe80::1$/.test(addr)

Fixed code:
  // 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)
  // 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));
  }

    || /^0177\./.test(addr)
    || /^0x7f\./i.test(addr)
    || /^fe80::1$/i.test(addr)

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;
};
  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);
    });
  });


    it('should correctly identify hexadecimal IP addresses like \'0x7f.1\' as private', () => {
      assert.equal(ip.isPrivate('0x7f.1'), true);
    });

  // 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);
  });