@@ -413,6 +413,17 @@ const getHeader = (headers: any, name: string): string | null | undefined => {
413413 return headers ?. [ name ] ?? headers ?. [ name . toLowerCase ( ) ]
414414}
415415
416+ // Egress IPs of our own relays (the web app's server-side fetches, the
417+ // managed proxy edge). Requests arriving from these carry the real visitor
418+ // IP in X-Forwarded-For; for every other connection the only trustworthy
419+ // source is X-Real-IP, which nginx derives from the TCP connection itself.
420+ export const TRUSTED_PROXY_IPS : ReadonlySet < string > = new Set (
421+ ( process . env . TRUSTED_PROXY_IPS || '' )
422+ . split ( ',' )
423+ . map ( ( entry ) => entry . trim ( ) )
424+ . filter ( Boolean ) ,
425+ )
426+
416427export const getIPFromHeaders = ( headers : unknown ) => {
417428 const customHeader = process . env . CLIENT_IP_HEADER
418429
@@ -423,7 +434,33 @@ export const getIPFromHeaders = (headers: unknown) => {
423434 }
424435 }
425436
426- return normalise ( getHeader ( headers , 'x-forwarded-for' ) )
437+ const realIp = normalise ( getHeader ( headers , 'x-real-ip' ) )
438+
439+ // Without X-Real-IP nothing identifies the connection peer, so
440+ // X-Forwarded-For is entirely client-supplied and cannot be trusted.
441+ // Callers fall back to the socket address instead.
442+ if ( ! realIp ) {
443+ return null
444+ }
445+
446+ if ( ! TRUSTED_PROXY_IPS . has ( realIp ) ) {
447+ return realIp
448+ }
449+
450+ // The connection is one of our own relays. Recover the visitor from
451+ // X-Forwarded-For, walking from the right past our own hops so a
452+ // client-supplied prefix never wins.
453+ const forwardedFor = String ( getHeader ( headers , 'x-forwarded-for' ) ?? '' )
454+ const hops = forwardedFor . split ( ',' )
455+
456+ for ( let i = hops . length - 1 ; i >= 0 ; -- i ) {
457+ const candidate = normalise ( hops [ i ] )
458+ if ( candidate && ! TRUSTED_PROXY_IPS . has ( candidate ) ) {
459+ return candidate
460+ }
461+ }
462+
463+ return realIp
427464}
428465
429466export const sumArrays = ( source : number [ ] , target : number [ ] ) => {
0 commit comments