如何正确使用HTTP_X_FORWARDED_FOR?

47

好的,我有一个小的身份验证问题。我的Web服务允许使用用户名和密码连接到我的API,但此连接也可以限制为特定的IP地址。

这意味着$_SERVER ['REMOTE_ADDR']可能是不正确的。我已经知道任何IP信息都不能真正依赖 - 我限制只是为了增加另一层安全性。

如果这是对我的Web服务器发出请求的概述:

clientSERVER => clientPROXY => myPROXY => mySERVER

那么这意味着mySERVER显示的是myPROXY的REMOTE_ADDR而非客户端的IP,并将客户端的实际IP作为HTTP_X_FORWARDED_FOR发送。

为了解决这个问题,我的Web服务有一个'受信任的代理' IP地址列表,如果REMOTE_ADDR来自其中一个受信任的IP地址,那么它告诉我的Web服务实际IP地址是HTTP_X_FORWARDED_FOR的值。

现在问题出现在clientPROXY上。这意味着(很常见)mySERVER得到具有多个IP地址的HTTP_X_FORWARDED_FOR值。根据HTTP_X_FORWARDED_FOR文档,该值是逗号分隔的IP地址列表,其中第一个IP是实际真实客户端的IP,每个其他IP地址都是代理的IP。

那么,如果HTTP_X_FORWARDED_FOR具有多个值并且我的服务受到IP限制,我是否必须检查HTTP_X_FORWARDED_FOR的'最后'值是否在我的允许IP列表中并忽略实际客户端IP?

我假设在需要设置允许的IP地址列表的系统中,白名单IP地址应该是代理的IP而不是代理后面的IP(因为那可能是某些本地主机IP并且经常更改)。

HTTP_CLIENT_IP怎么办?


https://dev59.com/bGs05IYBdhLWcg3wGuOd - BenMorel
7个回答

40

您可以使用此函数获取正确的客户端IP:

public function getClientIP(){       
     if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)){
            return  $_SERVER["HTTP_X_FORWARDED_FOR"];  
     }else if (array_key_exists('REMOTE_ADDR', $_SERVER)) { 
            return $_SERVER["REMOTE_ADDR"]; 
     }else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) {
            return $_SERVER["HTTP_CLIENT_IP"]; 
     } 

     return '';
}

9
这个回答没有解决问题描述中的具体情况,即并非所有请求都通过代理传递。因此,直接从客户端接收到的请求可能会在头部包含错误的IP地址。 - user149341
很棒的答案,你可以从Prestashop的Tools类中找到更准确的解决方法 :)。寻找getRemoteAddr()函数。 - Vipul Hadiya
8
这是一个可能存在的安全漏洞。任何人都可以在请求中添加X-Forwarded-For头部。 - frodeborli
@frodeborli:你说得对;这就是为什么应该在haproxy中使用reqidel+reqadd指令的原因。 - tisc0

26

我喜欢Hrishikesh的答案,只有这一点需要补充...因为我们发现在使用多个代理时会出现逗号分隔的字符串,所以我们需要添加一个explode并获取最终值,像这样:

$IParray=array_values(array_filter(explode(',',$_SERVER['HTTP_X_FORWARDED_FOR'])));
return reset($IParray);

array_filter是用来删除空条目的。


8
请注意,使用列表中的最后一个值仍然可能使用代理的IP。根据下面链接所说,原始客户端使用的是第一个IP地址。 http://en.wikipedia.org/wiki/X-Forwarded-For - Matthew Kolb
2
还要注意,同一来源表示这很容易伪造,因此最后一个更可靠。因此,每个用例可能会做出不同的选择。如果获取IP的用例是打击欺诈或垃圾邮件,则第一个IP可能毫无意义,而最可靠的地址 - 最后一个 - 最有用。如果获取IP的用例是较少恶意活动,则第一个IP将最有用。 - Rob Brandt

24

鉴于最新的httpoxy漏洞,确实需要一个完整的示例,演示如何正确使用HTTP_X_FORWARDED_FOR

因此,这里有一个用PHP编写的示例,演示如何检测客户端IP地址,如果您知道客户端可能在代理后面,并且您知道此代理可以信任。如果您不知道任何可信代理,请使用REMOTE_ADDR

<?php

function get_client_ip ()
{
    // Nothing to do without any reliable information
    if (!isset ($_SERVER['REMOTE_ADDR'])) {
        return NULL;
    }

    // Header that is used by the trusted proxy to refer to
    // the original IP
    $proxy_header = "HTTP_X_FORWARDED_FOR";

    // List of all the proxies that are known to handle 'proxy_header'
    // in known, safe manner
    $trusted_proxies = array ("2001:db8::1", "192.168.50.1");

    if (in_array ($_SERVER['REMOTE_ADDR'], $trusted_proxies)) {

        // Get the IP address of the client behind trusted proxy
        if (array_key_exists ($proxy_header, $_SERVER)) {

            // Header can contain multiple IP-s of proxies that are passed through.
            // Only the IP added by the last proxy (last IP in the list) can be trusted.
            $proxy_list = explode (",", $_SERVER[$proxy_header]);
            $client_ip = trim (end ($proxy_list));

            // Validate just in case
            if (filter_var ($client_ip, FILTER_VALIDATE_IP)) {
                return $client_ip;
            } else {
                // Validation failed - beat the guy who configured the proxy or
                // the guy who created the trusted proxy list?
                // TODO: some error handling to notify about the need of punishment
            }
        }
    }

    // In all other cases, REMOTE_ADDR is the ONLY IP we can trust.
    return $_SERVER['REMOTE_ADDR'];
}

print get_client_ip ();

?>

3
这是一个很好的回答,但有一个小问题。如果开启了严格错误报告,在一行上尝试对trim(end(explode()))进行操作会返回“只应通过引用传递变量”的错误。为了避免这种情况,请首先将分解的代理标题设置为变量,然后改用trim(end())来处理该变量。 - Ben Dyer
@BenDyer 谢谢,已修复。 - Erki Aring
2
我认为这是不正确的。根据 Mozilla 的说法,原始 IP 是第一个,而不是最后一个,所以应该是 trim(array_shift())。https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For - Lucas Bustamante
1
@LucasBustamante 虽然通常情况下,“原始IP”是最左边的,但是不能信任该IP,因为恶意行为者可以发送伪造的X-Forwarded_For头部。此头部中的任何虚假地址都将成为最左边的地址。如果避免这种潜在的伪造很重要,您需要最右边的地址(除了任何已知和信任的代理地址)。请参阅您提到的MDN文档中的“受信任代理计数/受信任代理列表”部分。 - thelr

3
您也可以通过使用mod_remoteip来通过Apache配置解决此问题,方法是将以下内容添加到conf.d文件中:
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 172.16.0.0/12
LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined

0
如果您在数据库中使用它,这是一个不错的方法:
将数据库中的IP字段设置为varchar(250),然后使用以下代码:
$theip = $_SERVER["REMOTE_ADDR"];

if (!empty($_SERVER["HTTP_X_FORWARDED_FOR"])) {
    $theip .= '('.$_SERVER["HTTP_X_FORWARDED_FOR"].')';
}

if (!empty($_SERVER["HTTP_CLIENT_IP"])) {
    $theip .= '('.$_SERVER["HTTP_CLIENT_IP"].')';
}

$realip = substr($theip, 0, 250);

然后你只需要将$realip与数据库IP字段进行比对


0
请注意,“HTTP_X_FORWARDED_FOR”可以很容易地被欺骗,例如使用浏览器插件更改条目...所以请注意!
尝试使用Hrishikesh Mishra的修改代码(未经测试):
 function getClientIP(){  

 if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) && preg_match('/\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}\b/', $_SERVER["HTTP_X_FORWARDED_FOR"])){
        return  $_SERVER["HTTP_X_FORWARDED_FOR"];  
 }else if (array_key_exists('REMOTE_ADDR', $_SERVER)) { 
        return $_SERVER["REMOTE_ADDR"]; 
 }else if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) {
        return $_SERVER["HTTP_CLIENT_IP"]; 
 } 

 return '';
}

-4

HTTP_CLIENT_IP 是获取用户 IP 地址最可靠的方式。其次是 HTTP_X_FORWARDED_FOR,然后是 REMOTE_ADDR。按照这个顺序检查所有三个,假设第一个设置了 (isset($_SERVER['HTTP_CLIENT_IP']) 如果该变量已设置,则返回 true) 就是正确的。您可以使用各种方法独立检查用户是否使用代理。请查看this


8
这些标头中没有一个比其他标头“更可靠”。它们都可以被客户端伪造,因此您的应用程序需要知道什么是可信的内容和什么是不可信的内容:通常您会了解自己的基础设施以及任何代理/负载均衡器的IP地址,这些设备位于您的HTTP服务器前面。 - BenMorel
1
真的,但通常如果要基于可靠性假设建立优先顺序,这就是选择。我知道它们都可以被操纵,但如果必须使用它们,就是这样做。 - TheEnvironmentalist
7
为什么这个话题上有那么多的假信息?$_SERVER['REMOTE_ADDR']是唯一一个不受远程用户影响、可靠的字段,而其他所有字段都是从头部解析出来的。 - transistor09
1
@transistor09 你愿意提供自己的答案吗? - TheEnvironmentalist
在我的网站上,$_SERVER['HTTP_CLIENT_IP'] 没有设置,而 $_SERVER['REMOTE_ADDR'] 在每次页面加载时都会改变,而 $_SERVER['HTTP_X_FORWARDED_FOR'] 是恒定的,并且设置为预期值。 - CragMonkey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接