diff --git a/package-lock.json b/package-lock.json index 0118c0f..f149d7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1002,9 +1002,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "version": "10.7.4", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz", + "integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==", "license": "MIT", "engines": { "node": ">= 12" diff --git a/package.json b/package.json index d54ba6b..a90bc2f 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "overrides": { "undici": "^6.28.0", "ws": "^8.21.0", - "shell-quote": "1.12.0" + "shell-quote": "1.12.0", + "ip-address": "10.7.4" }, "devDependencies": { "concurrently": "^9.2.4" diff --git a/test/ip-security.test.js b/test/ip-security.test.js new file mode 100644 index 0000000..46ab29d --- /dev/null +++ b/test/ip-security.test.js @@ -0,0 +1,33 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { createRequire } from "node:module"; +import { ipKeyGenerator } from "express-rate-limit"; + +// Exercise the limiter's actual dependency, including a nested installation. +const limiterRequire = createRequire(createRequire(import.meta.url).resolve("express-rate-limit")); +const { Address6 } = limiterRequire("ip-address"); + +// Run potentially blocking validation outside the test runner. The vulnerable +// version takes many seconds on this input; the patch rejects it in milliseconds. +test("oversized IPv6 zones with line terminators are rejected without stalling", () => { + const result = spawnSync(process.execPath, ["-e", ` + const assert = require('node:assert/strict'); + const { Address6 } = require(${JSON.stringify(limiterRequire.resolve("ip-address"))}); + for (const terminator of ['\\n', '\\r', '\\u2028', '\\u2029']) { + assert.equal(Address6.isValid('%'.repeat(200000) + terminator), false); + } + console.log('rejected all oversized zones'); + `], { cwd: fileURLToPath(new URL("../", import.meta.url)), encoding: "utf8", timeout: 2000 }); + assert.ifError(result.error); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.signal, null); + assert.match(result.stdout, /rejected all oversized zones/); +}); + +test("IPv6 zone parsing and existing limiter address grouping remain compatible", () => { + assert.equal(new Address6("fe80::1%eth0").zone, "%eth0"); + assert.equal(ipKeyGenerator("::ffff:192.0.2.1"), "192.0.2.1"); + assert.equal(ipKeyGenerator("2001:db8:abcd:1200::1"), ipKeyGenerator("2001:db8:abcd:12ff::2")); +});