@@ -9,8 +9,23 @@ const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
99const OPENROUTER_FETCH_TIMEOUT_MS = 10_000 ;
1010const 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 - 9 a - f A - F : ] + $ / ;
18+
19+ function isValidIpFormat ( ip : string ) : boolean {
20+ return IPV4_REGEX . test ( ip ) || IPV6_REGEX . test ( ip ) ;
21+ }
22+
1223if ( 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
1631if ( ! 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