From aa71ec3d3d5d14ae4e8f39131c6e0bf1b55224fe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 31 Jan 2026 07:06:53 +0000 Subject: [PATCH 1/6] Fix IPv6-safe rate limit keys Co-authored-by: me --- docs/skills/epic-security/SKILL.md | 12 +++++++++--- server/index.ts | 5 +++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/skills/epic-security/SKILL.md b/docs/skills/epic-security/SKILL.md index 6f599a64..33edc09f 100644 --- a/docs/skills/epic-security/SKILL.md +++ b/docs/skills/epic-security/SKILL.md @@ -196,7 +196,7 @@ Epic Stack uses `express-rate-limit` para prevenir abuso. ```typescript // server/index.ts -import rateLimit from 'express-rate-limit' +import rateLimit, { ipKeyGenerator } from 'express-rate-limit' const rateLimitDefault = { windowMs: 60 * 1000, // 1 minute @@ -205,7 +205,8 @@ const rateLimitDefault = { legacyHeaders: false, validate: { trustProxy: false }, keyGenerator: (req: express.Request) => { - return req.get('fly-client-ip') ?? `${req.ip}` + const clientIp = req.get('fly-client-ip') ?? req.ip + return ipKeyGenerator(clientIp) }, } @@ -508,13 +509,18 @@ export default function SignupRoute({ actionData }: Route.ComponentProps) { ```typescript // server/index.ts +import { ipKeyGenerator } from 'express-rate-limit' const apiRateLimit = rateLimit({ ...rateLimitDefault, windowMs: 60 * 1000, limit: 100, // 100 requests per minute for API keyGenerator: (req) => { const apiKey = req.get('X-API-Key') - return apiKey ?? req.get('fly-client-ip') ?? req.ip + if (apiKey) { + return apiKey + } + const clientIp = req.get('fly-client-ip') ?? req.ip + return ipKeyGenerator(clientIp) }, }) diff --git a/server/index.ts b/server/index.ts index 961903e0..7d1024f4 100644 --- a/server/index.ts +++ b/server/index.ts @@ -4,7 +4,7 @@ import chalk from 'chalk' import closeWithGrace from 'close-with-grace' import compression from 'compression' import express from 'express' -import rateLimit from 'express-rate-limit' +import rateLimit, { ipKeyGenerator } from 'express-rate-limit' import getPort, { portNumbers } from 'get-port' import helmet from 'helmet' import morgan from 'morgan' @@ -128,7 +128,8 @@ const rateLimitDefault = { // When sitting behind a CDN such as cloudflare, replace fly-client-ip with the CDN // specific header such as cf-connecting-ip keyGenerator: (req: express.Request) => { - return req.get('fly-client-ip') ?? `${req.ip}` + const clientIp = req.get('fly-client-ip') ?? req.ip + return ipKeyGenerator(clientIp) }, } From 1e735a26da23b9aedb729051b4166177ac504876 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 31 Jan 2026 07:08:01 +0000 Subject: [PATCH 2/6] Handle missing rate limit IP Co-authored-by: me --- server/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/index.ts b/server/index.ts index 7d1024f4..ed1c656f 100644 --- a/server/index.ts +++ b/server/index.ts @@ -128,7 +128,11 @@ const rateLimitDefault = { // When sitting behind a CDN such as cloudflare, replace fly-client-ip with the CDN // specific header such as cf-connecting-ip keyGenerator: (req: express.Request) => { - const clientIp = req.get('fly-client-ip') ?? req.ip + const clientIp = + req.get('fly-client-ip') ?? req.ip ?? req.socket.remoteAddress + if (!clientIp) { + return 'unknown' + } return ipKeyGenerator(clientIp) }, } From b365b8bc795e84336ecc5d940c0d8e5907db3d35 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 31 Jan 2026 07:08:47 +0000 Subject: [PATCH 3/6] Guard rate limit IP type Co-authored-by: me --- server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/index.ts b/server/index.ts index ed1c656f..73dda138 100644 --- a/server/index.ts +++ b/server/index.ts @@ -130,7 +130,7 @@ const rateLimitDefault = { keyGenerator: (req: express.Request) => { const clientIp = req.get('fly-client-ip') ?? req.ip ?? req.socket.remoteAddress - if (!clientIp) { + if (typeof clientIp !== 'string') { return 'unknown' } return ipKeyGenerator(clientIp) From 04df8d7ef4da14c7fbd0e72b72b59faba10e0b3f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 31 Jan 2026 07:09:25 +0000 Subject: [PATCH 4/6] Ensure rate limit IP fallback Co-authored-by: me --- server/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/index.ts b/server/index.ts index 73dda138..3a0f6ec6 100644 --- a/server/index.ts +++ b/server/index.ts @@ -129,11 +129,11 @@ const rateLimitDefault = { // specific header such as cf-connecting-ip keyGenerator: (req: express.Request) => { const clientIp = - req.get('fly-client-ip') ?? req.ip ?? req.socket.remoteAddress - if (typeof clientIp !== 'string') { - return 'unknown' - } - return ipKeyGenerator(clientIp) + req.get('fly-client-ip') ?? + req.ip ?? + req.socket.remoteAddress ?? + 'unknown' + return clientIp === 'unknown' ? clientIp : ipKeyGenerator(clientIp) }, } From 9251f8497fe11aa33b43158968755f6bf7f8da2f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 31 Jan 2026 07:10:21 +0000 Subject: [PATCH 5/6] Default rate limit IP fallback Co-authored-by: me --- server/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/index.ts b/server/index.ts index 3a0f6ec6..369215a6 100644 --- a/server/index.ts +++ b/server/index.ts @@ -132,8 +132,8 @@ const rateLimitDefault = { req.get('fly-client-ip') ?? req.ip ?? req.socket.remoteAddress ?? - 'unknown' - return clientIp === 'unknown' ? clientIp : ipKeyGenerator(clientIp) + '0.0.0.0' + return ipKeyGenerator(clientIp) }, } From b785721571f3da6309eb7010e211ed2f96ce4e92 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 31 Jan 2026 07:10:51 +0000 Subject: [PATCH 6/6] Annotate rate limit IP type Co-authored-by: me --- server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/index.ts b/server/index.ts index 369215a6..92d442f1 100644 --- a/server/index.ts +++ b/server/index.ts @@ -128,7 +128,7 @@ const rateLimitDefault = { // When sitting behind a CDN such as cloudflare, replace fly-client-ip with the CDN // specific header such as cf-connecting-ip keyGenerator: (req: express.Request) => { - const clientIp = + const clientIp: string = req.get('fly-client-ip') ?? req.ip ?? req.socket.remoteAddress ??