From 7fffd404e90e1c71682647a17b06f15904642297 Mon Sep 17 00:00:00 2001 From: DocFast CEO Date: Wed, 11 Mar 2026 20:06:44 +0100 Subject: [PATCH] =?UTF-8?q?chore:=20upgrade=20express-rate-limit=207.5.1?= =?UTF-8?q?=20=E2=86=92=208.3.1=20(IPv6=20security=20fix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixes IPv6 rate limit bypass vulnerability (GHSA-46wh-pxpv-q5gq) - IPv6 addresses now masked to /56 subnet by default - Updated custom keyGenerators to use ipKeyGenerator() helper - 5 new TDD tests for v8 features (ipKeyGenerator, IPv6 masking) - 672 tests passing, 0 TS errors, 0 npm audit vulnerabilities --- package-lock.json | 11 +++++--- package.json | 2 +- src/__tests__/rate-limit-v8.test.ts | 41 +++++++++++++++++++++++++++++ src/routes/billing.ts | 4 +-- src/routes/demo.ts | 4 +-- src/routes/email-change.ts | 4 +-- 6 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/rate-limit-v8.test.ts diff --git a/package-lock.json b/package-lock.json index 7ac34d5..a7d8814 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "compression": "^1.8.1", "express": "^5.1.0", - "express-rate-limit": "^7.5.1", + "express-rate-limit": "^8.3.1", "helmet": "^8.1.0", "marked": "^17.0.4", "nanoid": "^5.1.6", @@ -2339,10 +2339,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, "engines": { "node": ">= 16" }, diff --git a/package.json b/package.json index 48799a7..549ab03 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dependencies": { "compression": "^1.8.1", "express": "^5.1.0", - "express-rate-limit": "^7.5.1", + "express-rate-limit": "^8.3.1", "helmet": "^8.1.0", "marked": "^17.0.4", "nanoid": "^5.1.6", diff --git a/src/__tests__/rate-limit-v8.test.ts b/src/__tests__/rate-limit-v8.test.ts new file mode 100644 index 0000000..3f34d60 --- /dev/null +++ b/src/__tests__/rate-limit-v8.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +// Test express-rate-limit v8 upgrade compatibility +describe("express-rate-limit v8 upgrade", () => { + it("should export rateLimit as default export", async () => { + const mod = await import("express-rate-limit"); + expect(typeof mod.default).toBe("function"); + }); + + it("should export ipKeyGenerator helper (v8 feature)", async () => { + const mod = await import("express-rate-limit"); + // v8 exports ipKeyGenerator for IPv6 subnet masking + expect(typeof (mod as any).ipKeyGenerator).toBe("function"); + }); + + it("ipKeyGenerator should return IPv4 addresses unchanged", async () => { + const { ipKeyGenerator } = await import("express-rate-limit") as any; + const result = ipKeyGenerator("192.168.1.1"); + expect(result).toBe("192.168.1.1"); + }); + + it("ipKeyGenerator should mask IPv6 addresses to /56 by default", async () => { + const { ipKeyGenerator } = await import("express-rate-limit") as any; + const ip1 = ipKeyGenerator("2001:db8:85a3:1234:1111:2222:3333:4444"); + const ip2 = ipKeyGenerator("2001:db8:85a3:1256:aaaa:bbbb:cccc:dddd"); + // Same /56 prefix → same result + expect(ip1).toBe(ip2); + }); + + it("rateLimit should accept standardHeaders: true", async () => { + const { default: rateLimit } = await import("express-rate-limit"); + // Should not throw + const limiter = rateLimit({ + windowMs: 60000, + max: 100, + standardHeaders: true, + legacyHeaders: false, + }); + expect(typeof limiter).toBe("function"); + }); +}); diff --git a/src/routes/billing.ts b/src/routes/billing.ts index 3e3c7a0..46fdeff 100644 --- a/src/routes/billing.ts +++ b/src/routes/billing.ts @@ -1,5 +1,5 @@ import { Router, Request, Response } from "express"; -import rateLimit from "express-rate-limit"; +import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import Stripe from "stripe"; import { createProKey, downgradeByCustomer, updateEmailByCustomer, findKeyByCustomerId } from "../services/keys.js"; import logger from "../services/logger.js"; @@ -76,7 +76,7 @@ async function isDocFastSubscription(subscriptionId: string): Promise { const checkoutLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 3, - keyGenerator: (req: Request) => req.ip || req.socket.remoteAddress || "unknown", + keyGenerator: (req: Request) => ipKeyGenerator(req.ip || req.socket.remoteAddress || "unknown"), standardHeaders: true, legacyHeaders: false, message: { error: "Too many checkout requests. Please try again later." }, diff --git a/src/routes/demo.ts b/src/routes/demo.ts index 6ff375d..1ff9053 100644 --- a/src/routes/demo.ts +++ b/src/routes/demo.ts @@ -1,5 +1,5 @@ import { Router, Request, Response } from "express"; -import rateLimit from "express-rate-limit"; +import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import { renderPdf } from "../services/browser.js"; import { markdownToHtml, wrapHtml } from "../services/markdown.js"; import { handlePdfRoute, type SanitizedPdfOptions } from "../utils/pdf-handler.js"; @@ -56,7 +56,7 @@ const demoLimiter = rateLimit({ message: { error: "Demo limit reached (5/hour). Get a Pro API key at https://docfast.dev for unlimited access." }, standardHeaders: true, legacyHeaders: false, - keyGenerator: (req) => req.ip || req.socket.remoteAddress || "unknown", + keyGenerator: (req) => ipKeyGenerator(req.ip || req.socket.remoteAddress || "unknown"), }); router.use(demoLimiter); diff --git a/src/routes/email-change.ts b/src/routes/email-change.ts index 3041b15..72dcfb0 100644 --- a/src/routes/email-change.ts +++ b/src/routes/email-change.ts @@ -1,5 +1,5 @@ import { Router, Request, Response } from "express"; -import rateLimit from "express-rate-limit"; +import rateLimit, { ipKeyGenerator } from "express-rate-limit"; import { createPendingVerification, verifyCode } from "../services/verification.js"; import { sendVerificationEmail } from "../services/email.js"; import { queryWithRetry } from "../services/db.js"; @@ -13,7 +13,7 @@ const emailChangeLimiter = rateLimit({ message: { error: "Too many email change attempts. Please try again in 1 hour." }, standardHeaders: true, legacyHeaders: false, - keyGenerator: (req: Request) => req.body?.apiKey || req.ip || "unknown", + keyGenerator: (req: Request) => req.body?.apiKey || ipKeyGenerator(req.ip || "unknown"), }); const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;