-
-
Notifications
You must be signed in to change notification settings - Fork 5
Fix rate-limit bypass #645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8628be1
a626c43
88366b2
9162dc0
9393e14
7cb6edd
3b59d07
1ba1454
6b80816
ee54eec
8302416
4f97319
04de610
76c4e04
7c103dc
8e696e5
537b548
8034851
0f4e92f
ec3519e
664efc2
e535f6b
770d822
795001a
ebd3049
059948e
d200d7c
48ca3c4
7b5d722
020fefa
826a74b
76f7360
37c88fb
4575997
ec1e22a
c048027
3500ca5
bb0e999
0a44f88
3871cbc
181af3f
6722ceb
f9bba1e
8309c31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,131 @@ const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models"; | |
| const OPENROUTER_FETCH_TIMEOUT_MS = 10_000; | ||
| const TRUST_PROXY_MODE = process.env.TRUST_PROXY?.trim().toLowerCase(); | ||
|
|
||
| <<<<<<< HEAD | ||
| /** | ||
| * Basic IPv4/IPv6 format validation. | ||
| * Rejects obviously spoofed or malformed values used in x-forwarded-for. | ||
| */ | ||
| const IPV4_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/; | ||
| const IPV6_REGEX = /^[0-9a-fA-F:]+$/; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: IPv6 regex is too permissive — it matches any hex string (e.g., Prompt for AI agents |
||
|
|
||
| function isValidIpFormat(ip: string): boolean { | ||
| return IPV4_REGEX.test(ip) || IPV6_REGEX.test(ip); | ||
| } | ||
|
|
||
| if (TRUST_PROXY_MODE === "true") { | ||
| console.warn( | ||
| "[Models API] TRUST_PROXY=true uses x-forwarded-for for rate limiting. " + | ||
| "This is INSECURE unless the app is behind a trusted reverse proxy that overwrites x-forwarded-for. " + | ||
| "Prefer TRUST_PROXY=cloudflare or TRUST_PROXY=vercel for production deployments.", | ||
| ); | ||
| } | ||
|
|
||
| if (!TRUST_PROXY_MODE) { | ||
| console.warn("[Models API] TRUST_PROXY is unset; models endpoint will reject requests when IP is unavailable"); | ||
| } | ||
|
|
||
| if ( | ||
| TRUST_PROXY_MODE && | ||
| TRUST_PROXY_MODE !== "cloudflare" && | ||
| TRUST_PROXY_MODE !== "vercel" && | ||
| TRUST_PROXY_MODE !== "true" | ||
| ) { | ||
| console.warn("[Models API] Unrecognized TRUST_PROXY value; models endpoint will reject requests when IP is unavailable"); | ||
| } | ||
|
|
||
| const modelsIpRatelimit = upstashRedis | ||
| ? new Ratelimit({ | ||
| redis: upstashRedis, | ||
| limiter: Ratelimit.slidingWindow(30, "60 s"), | ||
| prefix: "ratelimit:models:ip", | ||
| }) | ||
| : null; | ||
|
|
||
| async function fetchModelsFromOpenRouter(): Promise<Response> { | ||
| try { | ||
| const response = await fetch(OPENROUTER_MODELS_URL, { | ||
| headers: { | ||
| Accept: "application/json", | ||
| }, | ||
| signal: AbortSignal.timeout(OPENROUTER_FETCH_TIMEOUT_MS), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| return json( | ||
| { error: "Upstream service error" }, | ||
| { status: 502 }, | ||
| ); | ||
| } | ||
|
|
||
| const payload = await response.text(); | ||
|
|
||
| if (upstashRedis) { | ||
| try { | ||
| await upstashRedis.set(MODELS_CACHE_KEY, payload, { | ||
| ex: MODELS_CACHE_TTL_SECONDS, | ||
| }); | ||
| } catch (error) { | ||
| console.warn("[Models API] Failed to write cache:", error); | ||
| } | ||
| } | ||
|
|
||
| return new Response(payload, { | ||
| status: 200, | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| "Cache-Control": "no-store", | ||
| }, | ||
| }); | ||
| } catch (error) { | ||
| console.warn("[Models API] OpenRouter fetch failed:", error); | ||
| return json({ error: "Upstream service unavailable" }, { status: 502 }); | ||
| } | ||
| } | ||
|
|
||
| function getClientIp(request: Request): string | null { | ||
| if (!TRUST_PROXY_MODE) { | ||
| return null; | ||
| } | ||
|
|
||
| if (TRUST_PROXY_MODE === "cloudflare") { | ||
| const cfConnectingIp = request.headers.get("cf-connecting-ip")?.trim(); | ||
| if (cfConnectingIp && isValidIpFormat(cfConnectingIp)) return cfConnectingIp; | ||
| return null; | ||
| } | ||
|
|
||
| if (TRUST_PROXY_MODE === "vercel") { | ||
| const vercelForwardedFor = request.headers.get("x-vercel-forwarded-for")?.trim(); | ||
| if (vercelForwardedFor) { | ||
| const first = vercelForwardedFor.split(",")[0]?.trim(); | ||
| if (first && isValidIpFormat(first)) return first; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| if (TRUST_PROXY_MODE === "true") { | ||
| // Prefer platform-specific headers that are harder to spoof, as they | ||
| // are typically set/overwritten by the edge proxy itself. | ||
| const cfIp = request.headers.get("cf-connecting-ip")?.trim(); | ||
| if (cfIp && isValidIpFormat(cfIp)) return cfIp; | ||
|
|
||
| const vercelIp = request.headers.get("x-vercel-forwarded-for")?.trim(); | ||
| if (vercelIp) { | ||
| const first = vercelIp.split(",")[0]?.trim(); | ||
| if (first && isValidIpFormat(first)) return first; | ||
| } | ||
|
|
||
| const realIp = request.headers.get("x-real-ip")?.trim(); | ||
| if (realIp && isValidIpFormat(realIp)) return realIp; | ||
|
|
||
| // Fall back to x-forwarded-for only as last resort, with IP validation. | ||
| // WARNING: This header is user-controlled unless the proxy overwrites it. | ||
| const forwardedFor = request.headers.get("x-forwarded-for")?.trim(); | ||
| if (forwardedFor) { | ||
| const first = forwardedFor.split(",")[0]?.trim(); | ||
| if (first && isValidIpFormat(first)) return first; | ||
| ||||||| 54e09ce | ||
| ======= | ||
| <<<<<<< HEAD | ||
| if (TRUST_PROXY_MODE === "true") { | ||
| console.warn("[Models API] TRUST_PROXY=true requires x-forwarded-for for rate limiting"); | ||
|
|
@@ -711,6 +836,7 @@ function getClientIp(request: Request): string | null { | |
| if (first) return first; | ||
| >>>>>>> main | ||
| >>>>>>> main | ||
| >>>>>>> main | ||
| >>>>>>> main | ||
| } | ||
| return null; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: IPv4 regex doesn't validate octet ranges (0–255), so values like
999.999.999.999pass validation. Consider using Node.jsnet.isIP(ip) !== 0which validates both IPv4 and IPv6 correctly, replacing both regex patterns with a single robust check.Prompt for AI agents