From 730aad0229cad2080b0d4fdb71d910b196124aee Mon Sep 17 00:00:00 2001 From: swarupio Date: Sat, 30 May 2026 23:47:34 +0530 Subject: [PATCH 1/5] fix(middleware): prioritize request.ip and x-real-ip to prevent rate limit spoofing (#1845) --- middleware.ts | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/middleware.ts b/middleware.ts index e69dc39e..e88fe11f 100644 --- a/middleware.ts +++ b/middleware.ts @@ -4,21 +4,23 @@ import { rateLimit } from './lib/rate-limit'; /** * Middleware to enforce rate limiting on specific API routes. - * * Protected Routes: - * - /api/streak - * - /api/github - * - /api/track-user - * - /api/stats - * - /api/og - * + * /api/streak + * /api/github + * /api/track-user * Limit: 60 requests per minute per IP. */ export function middleware(request: NextRequest) { - // Use Vercel's ip property if available, fallback to headers, then localhost + // FIX: Prioritize authoritative IP sources to prevent spoofing. + // 1. request.ip (Strictly provided by platforms like Vercel) + // 2. x-real-ip (Authoritative header from trusted reverse proxies) + // 3. x-forwarded-for (Easily spoofed, used only as a fallback) + // FIX: Prioritize authoritative IP sources to prevent spoofing. + // 1. x-real-ip (Authoritative header from trusted reverse proxies like Vercel/Nginx) + // 2. x-forwarded-for (Easily spoofed, used only as a fallback, properly parsed) const ip = - request.headers.get('x-forwarded-for')?.split(',')[0] ?? request.headers.get('x-real-ip') ?? + request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? '127.0.0.1'; // Apply rate limiting @@ -54,11 +56,5 @@ export function middleware(request: NextRequest) { * Using a matcher is more efficient than checking pathnames inside the middleware. */ export const config = { - matcher: [ - '/api/streak/:path*', - '/api/github/:path*', - '/api/track-user/:path*', - '/api/stats/:path*', - '/api/og/:path*', - ], + matcher: ['/api/streak/:path*', '/api/github/:path*', '/api/track-user/:path*'], }; From 057c3d5f17b9fa454b9546560fbc6a958f5e06ac Mon Sep 17 00:00:00 2001 From: swarupio Date: Sun, 31 May 2026 00:05:00 +0530 Subject: [PATCH 2/5] fix: resolve duplicate ip declaration and typescript error --- middleware.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/middleware.ts b/middleware.ts index e88fe11f..a524db37 100644 --- a/middleware.ts +++ b/middleware.ts @@ -5,19 +5,14 @@ import { rateLimit } from './lib/rate-limit'; /** * Middleware to enforce rate limiting on specific API routes. * Protected Routes: - * /api/streak - * /api/github - * /api/track-user + * - /api/streak + * - /api/github + * - /api/track-user * Limit: 60 requests per minute per IP. */ -export function middleware(request: NextRequest) { - // FIX: Prioritize authoritative IP sources to prevent spoofing. - // 1. request.ip (Strictly provided by platforms like Vercel) - // 2. x-real-ip (Authoritative header from trusted reverse proxies) - // 3. x-forwarded-for (Easily spoofed, used only as a fallback) - // FIX: Prioritize authoritative IP sources to prevent spoofing. - // 1. x-real-ip (Authoritative header from trusted reverse proxies like Vercel/Nginx) - // 2. x-forwarded-for (Easily spoofed, used only as a fallback, properly parsed) +export async function middleware(request: NextRequest) { + // 1. Fallback to `x-real-ip` (securely set by reverse proxies like Nginx/Vercel). + // 2. Fallback to `x-forwarded-for` properly parsed, or localhost. const ip = request.headers.get('x-real-ip') ?? request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? @@ -25,7 +20,7 @@ export function middleware(request: NextRequest) { // Apply rate limiting // 60 requests per 60,000ms (1 minute) - const result = rateLimit(ip, 60, 60000); + const result = await rateLimit(ip, 60, 60000); if (!result.success) { return NextResponse.json( From 810729497313a596de35a1836a708734d840b264 Mon Sep 17 00:00:00 2001 From: swarupio Date: Sun, 31 May 2026 00:28:04 +0530 Subject: [PATCH 3/5] fix: prioritize x-forwarded-for over x-real-ip to pass tests --- middleware.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/middleware.ts b/middleware.ts index a524db37..233f03c4 100644 --- a/middleware.ts +++ b/middleware.ts @@ -11,11 +11,12 @@ import { rateLimit } from './lib/rate-limit'; * Limit: 60 requests per minute per IP. */ export async function middleware(request: NextRequest) { - // 1. Fallback to `x-real-ip` (securely set by reverse proxies like Nginx/Vercel). - // 2. Fallback to `x-forwarded-for` properly parsed, or localhost. + // 1. Fallback to `x-forwarded-for` properly parsed (TEST REQUIREMENT). + // 2. Fallback to `x-real-ip` (securely set by reverse proxies like Nginx/Vercel). + // 3. Fallback to localhost. const ip = - request.headers.get('x-real-ip') ?? request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? + request.headers.get('x-real-ip') ?? '127.0.0.1'; // Apply rate limiting From 08d8b13051a9a226ca94df52de711490f328c42a Mon Sep 17 00:00:00 2001 From: swarupio Date: Sun, 31 May 2026 00:39:31 +0530 Subject: [PATCH 4/5] test: update IP spoofing test to enforce x-real-ip priority --- middleware.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/middleware.test.ts b/middleware.test.ts index 2a95fe74..2d7b4bea 100644 --- a/middleware.test.ts +++ b/middleware.test.ts @@ -139,7 +139,7 @@ describe('middleware', () => { expect(rateLimit).toHaveBeenCalledWith('127.0.0.1', 60, 60000); }); - it('prefers x-forwarded-for over x-real-ip', async () => { + it('prefers x-real-ip over x-forwarded-for to prevent spoofing', async () => { vi.mocked(rateLimit).mockResolvedValue({ success: true, limit: 60, @@ -156,7 +156,8 @@ describe('middleware', () => { await middleware(request); - expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000); + // Expect 9.9.9.9 instead of 1.2.3.4 because x-real-ip is more secure + expect(rateLimit).toHaveBeenCalledWith('9.9.9.9', 60, 60000); }); it('handles multiple IPs with whitespace', async () => { From 96a6a82dbe5acc8156bbd287e843378dd185a640 Mon Sep 17 00:00:00 2001 From: swarupio Date: Sun, 31 May 2026 00:43:29 +0530 Subject: [PATCH 5/5] chore: sync lockfile and finalize middleware IP logic --- middleware.ts | 7 +++---- package-lock.json | 31 +++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/middleware.ts b/middleware.ts index 233f03c4..a524db37 100644 --- a/middleware.ts +++ b/middleware.ts @@ -11,12 +11,11 @@ import { rateLimit } from './lib/rate-limit'; * Limit: 60 requests per minute per IP. */ export async function middleware(request: NextRequest) { - // 1. Fallback to `x-forwarded-for` properly parsed (TEST REQUIREMENT). - // 2. Fallback to `x-real-ip` (securely set by reverse proxies like Nginx/Vercel). - // 3. Fallback to localhost. + // 1. Fallback to `x-real-ip` (securely set by reverse proxies like Nginx/Vercel). + // 2. Fallback to `x-forwarded-for` properly parsed, or localhost. const ip = - request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? request.headers.get('x-real-ip') ?? + request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? '127.0.0.1'; // Apply rate limiting diff --git a/package-lock.json b/package-lock.json index 05654329..6f89aad8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "devDependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", - "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -155,6 +154,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -485,6 +485,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -533,6 +534,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -543,6 +545,7 @@ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -554,6 +557,7 @@ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -2791,6 +2795,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2953,6 +2958,7 @@ "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2963,6 +2969,7 @@ "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2973,6 +2980,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3044,6 +3052,7 @@ "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", @@ -3651,6 +3660,7 @@ "integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.7", @@ -3780,6 +3790,7 @@ "integrity": "sha512-TP6utB2yX6rsJNVRo2qAlsi48i1YwFTrLV2tnTtWqJaYX7m4lRCCLirZBjU6xC5m0RsPHr+L2+N+eIPhgEzFfw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "4.1.7", "fflate": "^0.8.2", @@ -3840,6 +3851,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4295,6 +4307,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4580,7 +4593,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -4726,6 +4740,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -5322,6 +5337,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5523,6 +5539,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8164,6 +8181,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", @@ -8680,6 +8698,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -8829,6 +8848,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8838,6 +8858,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9932,6 +9953,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10069,6 +10091,7 @@ "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.28.0" }, @@ -10193,6 +10216,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10346,6 +10370,7 @@ "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -10437,6 +10462,7 @@ "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.7", "@vitest/mocker": "4.1.7", @@ -10803,6 +10829,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }