Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib: use Node.js net lib and reject malformed addresses #144

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [ 12, 14, 16, 18 ]
node: [ 14, 16, 18, 20 ]
name: Node ${{ matrix.node }} sample
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ ip.or('192.168.1.134', '0.0.0.255') // 192.168.1.255
ip.isPrivate('127.0.0.1') // true
ip.isV4Format('127.0.0.1'); // true
ip.isV6Format('::ffff:127.0.0.1'); // true
ip.isValid('127.0.0.1'); // true

// operate on buffers in-place
var buf = new Buffer(128);
Expand All @@ -58,10 +59,17 @@ ip.cidrSubnet('192.168.1.134/26')
// range checking
ip.cidrSubnet('192.168.1.134/26').contains('192.168.1.190') // true


// ipv4 long conversion
ip.toLong('127.0.0.1'); // 2130706433
ip.fromLong(2130706433); // '127.0.0.1'

// malformed addresses and normalization
ip.normalizeStrict('0::01'); // '::1'
ip.isPrivate('0x7f.1'); // throw error
ip.isValidAndPrivate('0x7f.1'); // false
ip.normalizeStrict('0x7f.1'); // throw error
var normalized = ip.normalizeLax('0x7f.1'); // 127.0.0.1
ip.isPrivate(normalized); // true
```

### License
Expand Down
88 changes: 57 additions & 31 deletions lib/ip.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const ip = exports;
const { Buffer } = require('buffer');
const os = require('os');
const net = require('net');

ip.toBuffer = function (ip, buff, offset) {
offset = ~~offset;
Expand Down Expand Up @@ -82,15 +83,28 @@ ip.toString = function (buff, offset, length) {
return result;
};

const ipv4Regex = /^(\d{1,3}\.){3,3}\d{1,3}$/;
const ipv6Regex = /^(::)?(((\d{1,3}\.){3}(\d{1,3}){1})?([0-9a-f]){0,4}:{0,2}){1,8}(::)?$/i;
ip.isV4Format = net.isIPv4;

ip.isV4Format = function (ip) {
return ipv4Regex.test(ip);
ip.isV6Format = net.isIPv6;

ip.isValid = function (addr) {
return net.isIP(addr) !== 0;
};

ip.isV6Format = function (ip) {
return ipv6Regex.test(ip);
ip.normalizeStrict = function (addr) {
ouuan marked this conversation as resolved.
Show resolved Hide resolved
let family;
switch (net.isIP(addr)) {
case 4:
family = 'ipv4';
break;
case 6:
family = 'ipv6';
break;
default:
throw new Error(`Invalid ip address: ${addr}`);
}
const { address } = new net.SocketAddress({ address: addr, family });
ouuan marked this conversation as resolved.
Show resolved Hide resolved
return address;
};

function _normalizeFamily(family) {
Expand Down Expand Up @@ -306,26 +320,13 @@ ip.isEqual = function (a, b) {
};

ip.isPrivate = function (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);
}
addr = ip.normalizeStrict(addr);

// check private ranges
return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
|| /^(::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}:)?172\.(1[6-9]|2\d|30|31)\.([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)
Expand All @@ -337,16 +338,26 @@ ip.isPublic = function (addr) {
return !ip.isPrivate(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));
ip.isValidAndPrivate = function (addr) {
try {
return ip.isPrivate(addr);
} catch {
return false;
}
};

ip.isValidAndPublic = function (addr) {
try {
return ip.isPublic(addr);
} catch {
return false;
}
};

ip.isLoopback = function (addr) {
addr = ip.normalizeStrict(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)
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);
Expand Down Expand Up @@ -443,6 +454,8 @@ ip.fromLong = function (ipl) {
};

ip.normalizeToLong = function (addr) {
if (typeof addr !== 'string') return -1;

const parts = addr.split('.').map(part => {
// Handle hexadecimal format
if (part.startsWith('0x') || part.startsWith('0X')) {
Expand All @@ -469,6 +482,7 @@ ip.normalizeToLong = function (addr) {

switch (n) {
case 1:
if (parts[0] > 0xffffffff) return -1;
val = parts[0];
break;
case 2:
Expand All @@ -489,3 +503,15 @@ ip.normalizeToLong = function (addr) {

return val >>> 0;
};

ip.normalizeLax = function(addr) {
if (ip.isV6Format(addr)) {
const { address } = new net.SocketAddress({ address: addr, family: 'ipv6' });
return address;
}
const ipl = ip.normalizeToLong(addr);
if (ipl < 0) {
throw Error(`Invalid ip address: ${addr}`);
}
return ip.fromLong(ipl);
};
125 changes: 97 additions & 28 deletions test/api-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ describe('IP library for node.js', () => {
});
});

describe('normalizeIpv4() method', () => {
describe('normalizeToLong() method', () => {
// Testing valid inputs with different notations
it('should correctly normalize "127.0.0.1"', () => {
assert.equal(ip.normalizeToLong('127.0.0.1'), 2130706433);
Expand Down Expand Up @@ -352,8 +352,8 @@ describe('IP library for node.js', () => {
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);
it('should reject hexadecimal IP addresses like "0x7f.1"', () => {
assert.throws(() => ip.isPrivate('0x7f.1'));
});
});

Expand Down Expand Up @@ -469,41 +469,110 @@ describe('IP library for node.js', () => {
});
});

// 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);
});
describe('normalizeStrict() method', () => {
it('should keep valid IPv4 addresses', () => {
assert.equal(ip.normalizeStrict('1.1.1.1'), '1.1.1.1');
});

it('should return true for octal representation "0177.0.1"', () => {
assert.equal(ip.isLoopback('0177.0.1'), true);
});
it('should normalize IPv6 leading zeros', () => {
assert.equal(ip.normalizeStrict('00:0::000:01'), '::1');
});

it('should return true for octal representation "0177.1"', () => {
assert.equal(ip.isLoopback('0177.1'), true);
});
it('should normalize IPv6 letter casing', () => {
assert.equal(ip.normalizeStrict('aBCd::eF12'), 'abcd::ef12');
});

it('should normalize IPv6 addresses with embedded IPv4 addresses', () => {
assert.equal(ip.normalizeStrict('::ffff:7f00:1'), '::ffff:127.0.0.1');
assert.equal(ip.normalizeStrict('::1234:5678'), '::18.52.86.120');
});

// 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);
it('should reject malformed addresses', () => {
assert.throws(() => ip.normalizeStrict('127.0.1'));
assert.throws(() => ip.normalizeStrict('0x7f.1'));
assert.throws(() => ip.normalizeStrict('012.1'));
});
});

// IPv4 loopback in hexadecimal notation
it('should return true for hexadecimal representation "0x7f.0.1"', () => {
assert.equal(ip.isLoopback('0x7f.0.1'), true);
describe('normalizeLax() method', () => {
it('should normalize hex and oct addresses', () => {
assert.equal(ip.normalizeLax('0x7f.0x0.0x0.0x1'), '127.0.0.1');
assert.equal(ip.normalizeLax('012.34.0X56.0xAb'), '10.34.86.171');
});

it('should normalize 3-part addresses', () => {
assert.equal(ip.normalizeLax('192.168.1'), '192.168.0.1');
});

it('should normalize 2-part addresses', () => {
assert.equal(ip.normalizeLax('012.3'), '10.0.0.3');
assert.equal(ip.normalizeLax('012.0xabcdef'), '10.171.205.239');
});

it('should normalize single integer addresses', () => {
assert.equal(ip.normalizeLax('0x7f000001'), '127.0.0.1');
assert.equal(ip.normalizeLax('123456789'), '7.91.205.21');
assert.equal(ip.normalizeLax('01200034567'), '10.0.57.119');
});

it('should throw on invalid addresses', () => {
assert.throws(() => ip.normalizeLax('127.0.0xabcde'));
assert.throws(() => ip.normalizeLax('12345678910'));
assert.throws(() => ip.normalizeLax('0o1200034567'));
assert.throws(() => ip.normalizeLax('127.0.0.0.1'));
assert.throws(() => ip.normalizeLax('127.0.0.-1'));
assert.throws(() => ip.normalizeLax('-1'));
});

it('should normalize IPv6 leading zeros', () => {
assert.equal(ip.normalizeStrict('00:0::000:01'), '::1');
});
});

// IPv4 loopback in hexadecimal notation
it('should return true for hexadecimal representation "0x7f.1"', () => {
assert.equal(ip.isLoopback('0x7f.1'), true);
describe('isValid(), isV4Format()), isV6Format() methods', () => {
it('should validate ipv4 addresses', () => {
assert.equal(ip.isValid('1.1.1.1'), true);
assert.equal(ip.isValid('1.1.1.1.1'), false);
assert.equal(ip.isValid('1.1.1.256'), false);
assert.equal(ip.isValid('127.1'), false);
assert.equal(ip.isValid('127.0.0.01'), false);
assert.equal(ip.isValid('0x7f.0.0.1'), false);
assert.equal(ip.isV4Format('1.2.3.4'), true);
assert.equal(ip.isV6Format('1.2.3.4'), false);
});

it('should validate ipv6 addresses', () => {
assert.equal(ip.isValid('::1'), true);
assert.equal(ip.isValid('::1:1.2.3.4'), true);
assert.equal(ip.isValid('1::2::3'), false);
assert.equal(ip.isV4Format('::ffff:127.0.0.1'), false);
assert.equal(ip.isV6Format('::ffff:127.0.0.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);
describe('isValidAndPublic() method', () => {
it('should return true on valid public addresses', () => {
assert.equal(ip.isValidAndPublic('8.8.8.8'), true);
});
it('should return false on invalid addresses', () => {
assert.equal(ip.isValidAndPublic('8.8.8'), false);
assert.equal(ip.isValidAndPublic('8.8.8.010'), false);
});
it('should return false on valid private addresses', () => {
assert.equal(ip.isValidAndPublic('127.0.0.1'), false);
});
});

// IPv4 non-loopback address
it('should return false for "192.168.1.1"', () => {
assert.equal(ip.isLoopback('192.168.1.1'), false);
describe('isValidAndPrivate() method', () => {
it('should return true on valid private addresses', () => {
assert.equal(ip.isValidAndPrivate('192.168.1.2'), true);
});
it('should return false on invalid addresses', () => {
assert.equal(ip.isValidAndPrivate('127.1'), false);
assert.equal(ip.isValidAndPrivate('0x7f.0.0.1'), false);
});
it('should return false on valid public addresses', () => {
assert.equal(ip.isValidAndPrivate('8.8.8.8'), false);
});
});
});