chore: upgrade express-rate-limit 7.5.1 → 8.3.1 (IPv6 security fix)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m10s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m10s
- 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
This commit is contained in:
parent
603cbd7061
commit
7fffd404e9
6 changed files with 55 additions and 11 deletions
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -10,7 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^7.5.1",
|
"express-rate-limit": "^8.3.1",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"marked": "^17.0.4",
|
"marked": "^17.0.4",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
|
|
@ -2339,10 +2339,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-rate-limit": {
|
"node_modules/express-rate-limit": {
|
||||||
"version": "7.5.1",
|
"version": "8.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
|
||||||
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
"integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "10.1.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^7.5.1",
|
"express-rate-limit": "^8.3.1",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"marked": "^17.0.4",
|
"marked": "^17.0.4",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
|
|
|
||||||
41
src/__tests__/rate-limit-v8.test.ts
Normal file
41
src/__tests__/rate-limit-v8.test.ts
Normal file
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Router, Request, Response } from "express";
|
import { Router, Request, Response } from "express";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { createProKey, downgradeByCustomer, updateEmailByCustomer, findKeyByCustomerId } from "../services/keys.js";
|
import { createProKey, downgradeByCustomer, updateEmailByCustomer, findKeyByCustomerId } from "../services/keys.js";
|
||||||
import logger from "../services/logger.js";
|
import logger from "../services/logger.js";
|
||||||
|
|
@ -76,7 +76,7 @@ async function isDocFastSubscription(subscriptionId: string): Promise<boolean> {
|
||||||
const checkoutLimiter = rateLimit({
|
const checkoutLimiter = rateLimit({
|
||||||
windowMs: 60 * 60 * 1000, // 1 hour
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
max: 3,
|
max: 3,
|
||||||
keyGenerator: (req: Request) => req.ip || req.socket.remoteAddress || "unknown",
|
keyGenerator: (req: Request) => ipKeyGenerator(req.ip || req.socket.remoteAddress || "unknown"),
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
message: { error: "Too many checkout requests. Please try again later." },
|
message: { error: "Too many checkout requests. Please try again later." },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Router, Request, Response } from "express";
|
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 { renderPdf } from "../services/browser.js";
|
||||||
import { markdownToHtml, wrapHtml } from "../services/markdown.js";
|
import { markdownToHtml, wrapHtml } from "../services/markdown.js";
|
||||||
import { handlePdfRoute, type SanitizedPdfOptions } from "../utils/pdf-handler.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." },
|
message: { error: "Demo limit reached (5/hour). Get a Pro API key at https://docfast.dev for unlimited access." },
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
keyGenerator: (req) => req.ip || req.socket.remoteAddress || "unknown",
|
keyGenerator: (req) => ipKeyGenerator(req.ip || req.socket.remoteAddress || "unknown"),
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use(demoLimiter);
|
router.use(demoLimiter);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Router, Request, Response } from "express";
|
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 { createPendingVerification, verifyCode } from "../services/verification.js";
|
||||||
import { sendVerificationEmail } from "../services/email.js";
|
import { sendVerificationEmail } from "../services/email.js";
|
||||||
import { queryWithRetry } from "../services/db.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." },
|
message: { error: "Too many email change attempts. Please try again in 1 hour." },
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
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@]+$/;
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue