Skip to content

Commit f9bba1e

Browse files
authored
fix(api/models): prevent rate-limit bypass via header spoofing with IP validation
1 parent 6722ceb commit f9bba1e

1 file changed

Lines changed: 36 additions & 4 deletions

File tree

apps/web/src/routes/api/models.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,23 @@ const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
99
const OPENROUTER_FETCH_TIMEOUT_MS = 10_000;
1010
const TRUST_PROXY_MODE = process.env.TRUST_PROXY?.trim().toLowerCase();
1111

12+
/**
13+
* Basic IPv4/IPv6 format validation.
14+
* Rejects obviously spoofed or malformed values used in x-forwarded-for.
15+
*/
16+
const IPV4_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;
17+
const IPV6_REGEX = /^[0-9a-fA-F:]+$/;
18+
19+
function isValidIpFormat(ip: string): boolean {
20+
return IPV4_REGEX.test(ip) || IPV6_REGEX.test(ip);
21+
}
22+
1223
if (TRUST_PROXY_MODE === "true") {
13-
console.warn("[Models API] TRUST_PROXY=true requires x-forwarded-for for rate limiting");
24+
console.warn(
25+
"[Models API] TRUST_PROXY=true uses x-forwarded-for for rate limiting. " +
26+
"This is INSECURE unless the app is behind a trusted reverse proxy that overwrites x-forwarded-for. " +
27+
"Prefer TRUST_PROXY=cloudflare or TRUST_PROXY=vercel for production deployments.",
28+
);
1429
}
1530

1631
if (!TRUST_PROXY_MODE) {
@@ -82,23 +97,40 @@ function getClientIp(request: Request): string | null {
8297

8398
if (TRUST_PROXY_MODE === "cloudflare") {
8499
const cfConnectingIp = request.headers.get("cf-connecting-ip")?.trim();
85-
return cfConnectingIp || null;
100+
if (cfConnectingIp && isValidIpFormat(cfConnectingIp)) return cfConnectingIp;
101+
return null;
86102
}
87103

88104
if (TRUST_PROXY_MODE === "vercel") {
89105
const vercelForwardedFor = request.headers.get("x-vercel-forwarded-for")?.trim();
90106
if (vercelForwardedFor) {
91107
const first = vercelForwardedFor.split(",")[0]?.trim();
92-
if (first) return first;
108+
if (first && isValidIpFormat(first)) return first;
93109
}
94110
return null;
95111
}
96112

97113
if (TRUST_PROXY_MODE === "true") {
114+
// Prefer platform-specific headers that are harder to spoof, as they
115+
// are typically set/overwritten by the edge proxy itself.
116+
const cfIp = request.headers.get("cf-connecting-ip")?.trim();
117+
if (cfIp && isValidIpFormat(cfIp)) return cfIp;
118+
119+
const vercelIp = request.headers.get("x-vercel-forwarded-for")?.trim();
120+
if (vercelIp) {
121+
const first = vercelIp.split(",")[0]?.trim();
122+
if (first && isValidIpFormat(first)) return first;
123+
}
124+
125+
const realIp = request.headers.get("x-real-ip")?.trim();
126+
if (realIp && isValidIpFormat(realIp)) return realIp;
127+
128+
// Fall back to x-forwarded-for only as last resort, with IP validation.
129+
// WARNING: This header is user-controlled unless the proxy overwrites it.
98130
const forwardedFor = request.headers.get("x-forwarded-for")?.trim();
99131
if (forwardedFor) {
100132
const first = forwardedFor.split(",")[0]?.trim();
101-
if (first) return first;
133+
if (first && isValidIpFormat(first)) return first;
102134
}
103135
return null;
104136
}

0 commit comments

Comments
 (0)