1010use Nette ;
1111use Nette \Utils \Arrays ;
1212use Nette \Utils \Strings ;
13- use function array_filter , base64_encode , count , end , explode , file_get_contents , filter_input_array , filter_var , function_exists , get_debug_type , in_array , ini_get , is_array , is_string , key , min , preg_last_error , preg_match , preg_replace , preg_split , rtrim , sprintf , str_contains , strcasecmp , strlen , strncmp , strpos , strrpos , strtolower , strtr , substr , trim ;
13+ use function array_filter , array_key_last , array_map , base64_encode , explode , file_get_contents , filter_input_array , filter_var , function_exists , get_debug_type , in_array , ini_get , is_array , is_string , min , preg_last_error , preg_match , preg_replace , rtrim , sprintf , str_contains , strcasecmp , strlen , strncmp , strpos , strrpos , strtolower , strtr , substr , trim ;
1414use const PHP_SAPI ;
1515
1616
@@ -300,7 +300,8 @@ private function getClient(Url $url): array
300300 {
301301 $ remoteAddr = !empty ($ _SERVER ['REMOTE_ADDR ' ]) ? $ _SERVER ['REMOTE_ADDR ' ] : null ;
302302
303- // use real client address and host if trusted proxy is used
303+ // trust forwarding headers only when the request comes through a trusted proxy;
304+ // the proxy in turn should strip any forwarding header it does not set itself
304305 $ usingTrustedProxy = $ remoteAddr && Arrays::some ($ this ->proxies , fn (string $ proxy ): bool => Helpers::ipMatch ($ remoteAddr , $ proxy ));
305306 if ($ usingTrustedProxy ) {
306307 $ remoteHost = null ;
@@ -318,34 +319,42 @@ private function getClient(Url $url): array
318319
319320 private function useForwardedProxy (Url $ url ): ?string
320321 {
321- $ forwardParams = preg_split ('/[,;]/ ' , $ _SERVER ['HTTP_FORWARDED ' ]);
322- foreach ($ forwardParams as $ forwardParam ) {
323- [$ key , $ value ] = explode ('= ' , $ forwardParam , 2 ) + [1 => '' ];
324- $ proxyParams [strtolower (trim ($ key ))][] = trim ($ value , " \t\"" );
322+ // RFC 7239: split into hops (comma), each a set of params (semicolon)
323+ $ hops = $ addresses = [];
324+ foreach (explode (', ' , $ _SERVER ['HTTP_FORWARDED ' ]) as $ element ) {
325+ $ hop = [];
326+ foreach (explode ('; ' , $ element ) as $ pair ) {
327+ [$ key , $ value ] = explode ('= ' , $ pair , 2 ) + [1 => '' ];
328+ $ hop [strtolower (trim ($ key ))] = trim ($ value , " \t\"" );
329+ }
330+
331+ $ for = $ hop ['for ' ] ?? '' ;
332+ $ addresses [] = str_contains ($ for , '[ ' )
333+ ? substr ($ for , 1 , strpos ($ for , '] ' ) - 1 ) // IPv6 "[addr]:port"
334+ : explode (': ' , $ for )[0 ]; // IPv4 "addr:port" or bare address
335+ $ hops [] = $ hop ;
325336 }
326337
327- if (isset ($ proxyParams ['for ' ])) {
328- $ address = $ proxyParams ['for ' ][0 ];
329- $ remoteAddr = str_contains ($ address , '[ ' )
330- ? substr ($ address , 1 , strpos ($ address , '] ' ) - 1 ) // IPv6
331- : explode (': ' , $ address )[0 ]; // IPv4
338+ $ clientHop = $ this ->findClientHop ($ addresses );
339+ if ($ clientHop === null ) {
340+ return null ;
332341 }
333342
334- if (isset ($ proxyParams ['proto ' ]) && count ($ proxyParams ['proto ' ]) === 1 ) {
335- $ url ->setScheme (strcasecmp ($ proxyParams ['proto ' ][0 ], 'https ' ) === 0 ? 'https ' : 'http ' );
343+ // scheme and host from the client's own hop
344+ $ hop = $ hops [$ clientHop [0 ]];
345+ if (isset ($ hop ['proto ' ])) {
346+ $ url ->setScheme (strcasecmp ($ hop ['proto ' ], 'https ' ) === 0 ? 'https ' : 'http ' );
336347 $ url ->setPort ($ url ->getScheme () === 'https ' ? 443 : 80 );
337348 }
338349
339- if (
340- isset ($ proxyParams ['host ' ]) && count ($ proxyParams ['host ' ]) === 1
341- && ($ pair = $ this ->parseHostAndPort ($ proxyParams ['host ' ][0 ]))
342- ) {
350+ if (isset ($ hop ['host ' ]) && ($ pair = $ this ->parseHostAndPort ($ hop ['host ' ]))) {
343351 $ url ->setHost ($ pair [0 ]);
344352 if (isset ($ pair [1 ])) {
345353 $ url ->setPort ($ pair [1 ]);
346354 }
347355 }
348- return $ remoteAddr ?? null ;
356+
357+ return $ clientHop [1 ];
349358 }
350359
351360
@@ -360,32 +369,50 @@ private function useNonstandardProxy(Url $url): ?string
360369 $ url ->setPort ((int ) $ _SERVER ['HTTP_X_FORWARDED_PORT ' ]);
361370 }
362371
363- if (!empty ($ _SERVER ['HTTP_X_FORWARDED_FOR ' ])) {
364- $ xForwardedForWithoutProxies = array_filter (
365- explode (', ' , $ _SERVER ['HTTP_X_FORWARDED_FOR ' ]),
366- fn (string $ ip ): bool => filter_var ($ ip = trim ($ ip ), FILTER_VALIDATE_IP ) === false
367- || !Arrays::some ($ this ->proxies , fn (string $ proxy ): bool => Helpers::ipMatch ($ ip , $ proxy )),
368- );
369- if ($ xForwardedForWithoutProxies ) {
370- $ remoteAddr = trim (end ($ xForwardedForWithoutProxies ));
371- $ xForwardedForRealIpKey = key ($ xForwardedForWithoutProxies );
372- }
372+ if (empty ($ _SERVER ['HTTP_X_FORWARDED_FOR ' ])) {
373+ return null ;
373374 }
374375
375- if (isset ($ xForwardedForRealIpKey ) && !empty ($ _SERVER ['HTTP_X_FORWARDED_HOST ' ])) {
376- $ xForwardedHost = explode (', ' , $ _SERVER ['HTTP_X_FORWARDED_HOST ' ]);
377- if (
378- isset ($ xForwardedHost [$ xForwardedForRealIpKey ])
379- && ($ pair = $ this ->parseHostAndPort (trim ($ xForwardedHost [$ xForwardedForRealIpKey ])))
380- ) {
376+ $ clientHop = $ this ->findClientHop (array_map (trim (...), explode (', ' , $ _SERVER ['HTTP_X_FORWARDED_FOR ' ])));
377+ if ($ clientHop === null ) {
378+ return null ;
379+ }
380+
381+ if (!empty ($ _SERVER ['HTTP_X_FORWARDED_HOST ' ])) {
382+ $ hosts = explode (', ' , $ _SERVER ['HTTP_X_FORWARDED_HOST ' ]);
383+ if (isset ($ hosts [$ clientHop [0 ]]) && ($ pair = $ this ->parseHostAndPort (trim ($ hosts [$ clientHop [0 ]])))) {
381384 $ url ->setHost ($ pair [0 ]);
382385 if (isset ($ pair [1 ])) {
383386 $ url ->setPort ($ pair [1 ]);
384387 }
385388 }
386389 }
387390
388- return $ remoteAddr ?? null ;
391+ return $ clientHop [1 ];
392+ }
393+
394+
395+ /**
396+ * Returns [index, address] of the rightmost hop after stripping trailing trusted proxies,
397+ * or null when that hop is not a valid IP.
398+ * @param list<string> $addresses
399+ * @return array{int, string}|null
400+ */
401+ private function findClientHop (array $ addresses ): ?array
402+ {
403+ $ untrusted = array_filter (
404+ $ addresses ,
405+ fn (string $ ip ): bool => filter_var ($ ip , FILTER_VALIDATE_IP ) === false
406+ || !Arrays::some ($ this ->proxies , fn (string $ proxy ): bool => Helpers::ipMatch ($ ip , $ proxy )),
407+ );
408+ if (!$ untrusted ) {
409+ return null ;
410+ }
411+
412+ $ index = array_key_last ($ untrusted );
413+ return filter_var ($ untrusted [$ index ], FILTER_VALIDATE_IP ) === false
414+ ? null
415+ : [$ index , $ untrusted [$ index ]];
389416 }
390417
391418
0 commit comments