From b00349b04770a212ee1405c330d508d4928ae43f Mon Sep 17 00:00:00 2001 From: Joseph Petersen Date: Tue, 10 Mar 2026 16:07:08 +0100 Subject: [PATCH 1/2] Remove range_check dependency Replace range_check with a custom CIDR check using neoip's toUInt8Array and byte-level prefix matching. This removes a dependency with a broken CJS build (ip6 is ESM-only) and adds IPv6 CIDR support. --- lib/ipfilter.js | 24 +++++++++++-- package-lock.json | 86 +++++++++++++++++++++++++++-------------------- package.json | 5 +-- 3 files changed, 74 insertions(+), 41 deletions(-) diff --git a/lib/ipfilter.js b/lib/ipfilter.js index 51416da..edba0ba 100644 --- a/lib/ipfilter.js +++ b/lib/ipfilter.js @@ -10,7 +10,6 @@ * Module dependencies. */ const iputil = require('neoip') -const rangeCheck = require('range_check') const IpDeniedError = require('./deniedError') const proxyaddr = require('proxy-addr') @@ -97,8 +96,27 @@ module.exports = function ipfilter(ips, opts) { } } + const inCidr = (addr, cidr) => { + const [network, prefixStr] = cidr.split('/') + const prefix = parseInt(prefixStr, 10) + const addrBytes = iputil.toUInt8Array(addr) + const netBytes = iputil.toUInt8Array(network) + if (addrBytes.length !== netBytes.length) return false + const fullBytes = Math.floor(prefix / 8) + for (let i = 0; i < fullBytes; i++) { + if (addrBytes[i] !== netBytes[i]) return false + } + const remainBits = prefix % 8 + if (remainBits > 0) { + const mask = 0xff << (8 - remainBits) + if ((addrBytes[fullBytes] & mask) !== (netBytes[fullBytes] & mask)) + return false + } + return true + } + const testCidrBlock = (ip, constraint, mode) => { - if (rangeCheck.inRange(ip, constraint)) { + if (inCidr(stripPort(ip), constraint)) { return mode === 'allow' } else { return mode === 'deny' @@ -154,7 +172,7 @@ module.exports = function ipfilter(ips, opts) { // Check if it is an array or a string if (typeof constraint === 'string') { - if (rangeCheck.isRange(constraint)) { + if (constraint.includes('/')) { return testCidrBlock(ip, constraint, mode) } else { return testExplicitIp(ip, constraint, mode) diff --git a/package-lock.json b/package-lock.json index 2311271..900ea25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,15 @@ "license": "MIT", "dependencies": { "neoip": "^3.1.0", - "proxy-addr": "^2.0.7", - "range_check": "^2.0.4" + "proxy-addr": "^2.0.7" }, "devDependencies": { + "@eslint/js": "^10.0.1", "eslint": "^10.0.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-jest": "^29.12.1", "eslint-plugin-prettier": "^5.5.4", + "globals": "^17.4.0", "husky": "^9.1.7", "jest": "^30.2.0", "lint-staged": "^16.2.7", @@ -693,6 +694,27 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, "node_modules/@eslint/object-schema": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", @@ -3265,6 +3287,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3369,14 +3404,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ip6": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/ip6/-/ip6-0.2.10.tgz", - "integrity": "sha512-1LdpyKjhvepd6EbAU6rW4g14vuYtx5TnJX9TfZZBhsM6DsyPQLNzW12rtbUqXBMwqFrLVV/Gcxv0GNFvJp2cYA==", - "bin": { - "ip6": "ip6-cli.js" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4938,18 +4965,6 @@ ], "license": "MIT" }, - "node_modules/range_check": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/range_check/-/range_check-2.0.4.tgz", - "integrity": "sha512-aed0ocXXj+SIiNNN9b+mZWA3Ow2GXHtftOGk2xQwshK5GbEZAvUcPWNQBLTx/lPcdFRIUFlFCRtHTQNIFMqynQ==", - "dependencies": { - "ip6": "^0.2.0", - "ipaddr.js": "^1.9.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -6402,6 +6417,13 @@ "@types/json-schema": "^7.0.15" } }, + "@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "requires": {} + }, "@eslint/object-schema": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", @@ -8102,6 +8124,12 @@ } } }, + "globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true + }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -8166,11 +8194,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "ip6": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/ip6/-/ip6-0.2.10.tgz", - "integrity": "sha512-1LdpyKjhvepd6EbAU6rW4g14vuYtx5TnJX9TfZZBhsM6DsyPQLNzW12rtbUqXBMwqFrLVV/Gcxv0GNFvJp2cYA==" - }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9247,15 +9270,6 @@ "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", "dev": true }, - "range_check": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/range_check/-/range_check-2.0.4.tgz", - "integrity": "sha512-aed0ocXXj+SIiNNN9b+mZWA3Ow2GXHtftOGk2xQwshK5GbEZAvUcPWNQBLTx/lPcdFRIUFlFCRtHTQNIFMqynQ==", - "requires": { - "ip6": "^0.2.0", - "ipaddr.js": "^1.9.1" - } - }, "react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", diff --git a/package.json b/package.json index 27baca9..7b5d64c 100644 --- a/package.json +++ b/package.json @@ -50,14 +50,15 @@ }, "dependencies": { "neoip": "^3.1.0", - "proxy-addr": "^2.0.7", - "range_check": "^2.0.4" + "proxy-addr": "^2.0.7" }, "devDependencies": { + "@eslint/js": "^10.0.1", "eslint": "^10.0.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-jest": "^29.12.1", "eslint-plugin-prettier": "^5.5.4", + "globals": "^17.4.0", "husky": "^9.1.7", "jest": "^30.2.0", "lint-staged": "^16.2.7", From b8b1b4f67039c94b782147343968edebbd5a25d8 Mon Sep 17 00:00:00 2001 From: Joseph Petersen Date: Tue, 10 Mar 2026 16:11:15 +0100 Subject: [PATCH 2/2] Add CIDR boundary tests for inCidr implementation Cover IPv4/IPv6 CIDR boundaries, /32 and /128 single address blocks, and IPv4 vs IPv6 mismatch cases. --- test/ipfilter.spec.js | 146 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/test/ipfilter.spec.js b/test/ipfilter.spec.js index 163df74..cd6e57a 100644 --- a/test/ipfilter.spec.js +++ b/test/ipfilter.spec.js @@ -1103,6 +1103,152 @@ describe('an array of cidr blocks', () => { }) }) +describe('cidr boundary checks', () => { + describe('IPv4 /28 boundaries', () => { + beforeEach(() => { + // 192.168.1.0/28 covers 192.168.1.0 - 192.168.1.15 + ipfilter = IpFilter(['192.168.1.0/28'], { + log: false, + mode: 'allow', + }) + req = { + session: {}, + headers: [], + connection: { + remoteAddress: '', + }, + } + }) + + it('should allow ip at network address', (done) => { + req.connection.remoteAddress = '192.168.1.0' + ipfilter(req, {}, () => { + done() + }) + }) + + it('should allow ip at broadcast address', (done) => { + req.connection.remoteAddress = '192.168.1.15' + ipfilter(req, {}, () => { + done() + }) + }) + + it('should allow ip in middle of block', (done) => { + req.connection.remoteAddress = '192.168.1.8' + ipfilter(req, {}, () => { + done() + }) + }) + + it('should deny ip one above block', (done) => { + req.connection.remoteAddress = '192.168.1.16' + checkError(ipfilter, req, done) + }) + + it('should deny ip in different subnet', (done) => { + req.connection.remoteAddress = '192.168.2.1' + checkError(ipfilter, req, done) + }) + }) + + describe('IPv6 /64 boundaries', () => { + beforeEach(() => { + ipfilter = IpFilter(['2001:db8:1234:5678::/64'], { + log: false, + mode: 'allow', + }) + req = { + session: {}, + headers: [], + connection: { + remoteAddress: '', + }, + } + }) + + it('should allow ip within the /64 block', (done) => { + req.connection.remoteAddress = '2001:db8:1234:5678::1' + ipfilter(req, {}, () => { + done() + }) + }) + + it('should allow ip at end of /64 block', (done) => { + req.connection.remoteAddress = '2001:db8:1234:5678:ffff:ffff:ffff:ffff' + ipfilter(req, {}, () => { + done() + }) + }) + + it('should deny ip in different /64 block', (done) => { + req.connection.remoteAddress = '2001:db8:1234:5679::1' + checkError(ipfilter, req, done) + }) + + it('should deny IPv4 address against IPv6 cidr', (done) => { + req.connection.remoteAddress = '192.168.1.1' + checkError(ipfilter, req, done) + }) + }) + + describe('IPv6 /128 single address', () => { + beforeEach(() => { + ipfilter = IpFilter(['2001:db8::1/128'], { + log: false, + mode: 'allow', + }) + req = { + session: {}, + headers: [], + connection: { + remoteAddress: '', + }, + } + }) + + it('should allow the exact address', (done) => { + req.connection.remoteAddress = '2001:db8::1' + ipfilter(req, {}, () => { + done() + }) + }) + + it('should deny any other address', (done) => { + req.connection.remoteAddress = '2001:db8::2' + checkError(ipfilter, req, done) + }) + }) + + describe('IPv4 /32 single address', () => { + beforeEach(() => { + ipfilter = IpFilter(['10.0.0.1/32'], { + log: false, + mode: 'allow', + }) + req = { + session: {}, + headers: [], + connection: { + remoteAddress: '', + }, + } + }) + + it('should allow the exact address', (done) => { + req.connection.remoteAddress = '10.0.0.1' + ipfilter(req, {}, () => { + done() + }) + }) + + it('should deny any other address', (done) => { + req.connection.remoteAddress = '10.0.0.2' + checkError(ipfilter, req, done) + }) + }) +}) + describe('mixing different types of filters', () => { describe('with a whitelist', () => { beforeEach(() => {