From ee3a982b0a78660756441b9d3aa7970ebb248433 Mon Sep 17 00:00:00 2001 From: anshul23102 Date: Tue, 19 May 2026 06:45:20 +0530 Subject: [PATCH] fix(security): prevent rate limit bypass via X-Forwarded-For spoofing The public profile endpoint used x-forwarded-for as the primary key for IP-based rate limiting. This header is fully client-controlled: an attacker can set it to a different value on every request, making the 30 req/min limit completely ineffective. Switch to req.ip as the primary key. The Next.js/Vercel runtime derives req.ip from the verified network-layer source address and it cannot be set by the caller. Fall back to x-real-ip only when req.ip is unavailable. This prevents unauthenticated callers from exhausting the shared GITHUB_TOKEN quota (5 000 req/hr) and degrading service for all users. --- src/app/api/public/[username]/route.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/app/api/public/[username]/route.ts b/src/app/api/public/[username]/route.ts index 62f3fc3..0298587 100644 --- a/src/app/api/public/[username]/route.ts +++ b/src/app/api/public/[username]/route.ts @@ -19,12 +19,15 @@ const RATE_LIMIT_REQUESTS = 30; const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute function getRateLimitKey(req: NextRequest): string { - return ( - req.headers.get("x-forwarded-for") || - req.headers.get("x-real-ip") || - req.ip || - "unknown" - ); + // req.ip is populated by the Next.js / Vercel runtime from the verified + // network-layer source address and cannot be spoofed by the caller. + // + // x-forwarded-for is intentionally excluded here: it is a plain request + // header that any client can set to an arbitrary value. Trusting it as the + // primary key allows an attacker to rotate the header on every request, + // bypass the per-IP limit entirely, and exhaust the shared GITHUB_TOKEN + // quota (5 000 req/hr), making the endpoint unavailable for all users. + return req.ip || req.headers.get("x-real-ip") || "unknown"; } function checkRateLimit(ip: string): { allowed: boolean; retryAfter?: number } {