From 9d6d8b997685a4d707d458f8a5252418a3662ad0 Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Mon, 18 May 2026 01:23:44 -0500 Subject: [PATCH] fix(server): use ipKeyGenerator helper for IPv6-safe rate limiting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server failed to boot after the dependabot bump of express-rate-limit (and was already broken on the current 8.5.x): ValidationError: Custom keyGenerator appears to use request IP without calling the ipKeyGenerator helper function for IPv6 addresses. This could allow IPv6 users to bypass limits. ERR_ERL_KEY_GEN_IPV6 express-rate-limit 8.x validates that any custom `keyGenerator` that touches `req.ip` routes the value through the package's `ipKeyGenerator()` helper. The helper: - returns IPv4 addresses verbatim - normalizes IPv6 to a /64 prefix (so a single client can't bypass per-IP limits by rotating through their allocated /64 block) Without the helper, IPv6 clients could each present a unique address and slip past the rate-limit budget — a real bypass, not just a deprecation warning. Authenticated keying (sha256 hash of the authKey header) is unchanged; the IPv6 fix only affects the anonymous fallback path. Unit tests in `tests/unit/rate-limit-key.test.js` updated mocks to return the normalized form and still pass on all 7 cases. Verified locally: DB_PASSWORD=x node server.js → "Server listening" on 0.0.0.0:3000 (Previously this exited 1 with the validation error.) Tests: 479 pass / 4 skip. Lint clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/middleware/rate-limit-key.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/middleware/rate-limit-key.js b/app/middleware/rate-limit-key.js index 71afccd..ff1e65e 100644 --- a/app/middleware/rate-limit-key.js +++ b/app/middleware/rate-limit-key.js @@ -17,18 +17,33 @@ * keyspace separation and keeps the raw token out of any * downstream rate-limiter store. * + * IPv6 handling: express-rate-limit v8+ refuses to start unless any + * custom keyGenerator that touches `req.ip` routes the value through + * `ipKeyGenerator(req, res)`. The helper canonicalizes IPv4 and IPv6 + * addresses (matching them to a /64 prefix on IPv6 to prevent a + * single client from bypassing limits by rotating through their /64 + * allocation). Without this wrapper, IPv6 clients could each present + * a unique address and slip past the per-IP budget. + * * Exported separately from server.js so unit tests can exercise * the keying directly without spinning up an HTTP server. */ const crypto = require('crypto'); +const { ipKeyGenerator } = require('express-rate-limit'); function keyByAuthKeyOrIp(req /*, res */) { const authKey = req.get && req.get('authKey'); if (authKey) { return 'k:' + crypto.createHash('sha256').update(authKey).digest('hex').slice(0, 16); } - return 'ip:' + (req.ip || (req.connection && req.connection.remoteAddress) || 'unknown'); + // express-rate-limit v8+ requires the helper. It takes the raw + // IP string and returns the IPv4 address verbatim or the IPv6 + // /64 prefix. Fall back to 'unknown' when no source IP is + // available (e.g. unit-test fixtures or non-IP transports). + const ip = req.ip || (req.connection && req.connection.remoteAddress); + if (!ip) return 'ip:unknown'; + return 'ip:' + ipKeyGenerator(ip); } module.exports = { keyByAuthKeyOrIp };