PHP $ _SERVER ['HTTP_HOST'] vs. $ _SERVER ['SERVER_NAME'],我是否正确理解手册页?


167

我做了很多搜索,还阅读了PHP $ _SERVER文档。对于在整个站点中使用的简单链接定义的PHP脚本,我是否有权使用该脚本?

$_SERVER['SERVER_NAME'] 基于您的Web服务器的配置文件(在我的情况下为Apache2),并根据以下指令而有所不同:(1)VirtualHost,(2)ServerName,(3)UseCanonicalName等。

$_SERVER['HTTP_HOST'] 基于客户端的请求。

因此,在我看来,为了使我的脚本尽可能兼容而使用的适当的脚本将是$_SERVER['HTTP_HOST']。这个假设正确吗?

后续评论:

我想我在读完这篇文章并注意到有些人说“他们不会信任任何一个$_SERVER变量” 后,我有点偏执:

显然,这里的讨论主要是关于$_SERVER['PHP_SELF']为什么不适当地转义以防止XSS攻击就不应该在form action属性中使用它的原因。

我对上述原始问题的结论是$_SERVER['HTTP_HOST'],即使在表单中使用XSS攻击,也不必担心XSS攻击是安全的。

如果我错了,请纠正我。

Answers:


149

那可能是每个人的第一个想法。但这有点困难。参见Chris Shiflett的文章SERVER_NAMEVersusHTTP_HOST

似乎没有灵丹妙药。只有当您强迫Apache使用规范名称时,您才始终使用来获得正确的服务器名称SERVER_NAME

因此,您要么选择该名称,要么对照白名单检查主机名:

$allowed_hosts = array('foo.example.com', 'bar.example.com');
if (!isset($_SERVER['HTTP_HOST']) || !in_array($_SERVER['HTTP_HOST'], $allowed_hosts)) {
    header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request');
    exit;
}

4
大声笑,我读了这篇文章,但它似乎并没有真正回答我的问题。专业开发人员使用哪一个?如果有的话。
Jeff

2
有趣的是,我不知道SERVER_NAME默认在Apache中使用用户提供的值。
Powerlord

1
@Jeff,对于承载多个子域的服务器,您只有两种选择$_SERVER['SERVER_NAME']$_SERVER['HTTP_HOST'](除了根据用户请求实现一些其他自定义握手之外)。专业开发人员不信任他们无法完全理解的事物。因此,他们要么完全正确地设置了SAPI (在这种情况下,他们使用的选项给出正确的结果),要么将它们列入白名单,以至于SAPI的供给值无关紧要。
Pacerier'3

@Gumbo,由于某些SAPI的严重问题,您需要应用“端口”补丁。另外,array_key_exists更具可扩展性相比,in_array具有O(n)性能。
Pacerier,2015年

2
@Pacerier array_key_exists和in_array做不同的事情,前者检查键,后者检查值,因此您不能仅仅交换它们。另外,如果您有两个值的数组,则不必担心O(n)的性能……
eis

74

只是需要特别说明-如果服务器在80以外的端口上运行(这在开发/内联网计算机上可能很常见),则HTTP_HOST包含该端口,而SERVER_NAME没有。

$_SERVER['HTTP_HOST'] == 'localhost:8080'
$_SERVER['SERVER_NAME'] == 'localhost'

(至少这是我在基于Apache端口的虚拟主机中注意到的内容)

正如迈克下面提到的,HTTP_HOST没有包含:443在HTTPS运行时(除非你是一个非标准端口,我没有测试运行)。


4
注意:HTTP_HOST中的443端口也不存在(默认SSL端口)。
Mike

因此,换句话说,的值HTTP_HOST不完全Host:是用户提供的参数。它只是基于此。
Pacerier 2015年

1
@Pacerier否,相反:HTTP_HOST正是HTTP请求随附的Host:字段。该端口是该端口的一部分,当默认端口为默认端口时(HTTP为80; HTTPS为443),浏览器不会提及它
xhienne

29

使用其中之一。它们都是同等安全的,因为在许多情况下SERVER_NAME都是从HTTP_HOST填充而来的。我通常会使用HTTP_HOST,这样用户就可以使用启动时使用的确切主机名。例如,如果我在.com和.org域中具有相同的站点,则我不想将某人从.org发送到.com,尤其是如果他们在.org上可能有登录令牌,如果他们将这些令牌发送到另一个域。

无论哪种方式,您都只需要确保您的Web应用程序仅能响应已知良好的域即可。可以通过(a)应用程序侧检查(例如Gumbo的检查)来完成此操作,或者(b)通过在您想要的域名上使用不响应给出未知Host标头的请求的虚拟主机来完成

这样做的原因是,如果允许以任何旧名称访问您的站点,那么您就容易受到DNS重新绑定攻击的攻击(其他站点的主机名指向您的IP,用户使用攻击者的主机名访问该站点,然后使用主机名被转移到攻击者的IP,使用您的cookie / auth进行身份验证)和搜索引擎劫持(攻击者将自己的主机名指向您的站点,并试图使搜索引擎将其视为“最佳”主要主机名)。

显然,这里的讨论主要是关于$ _SERVER ['PHP_SELF']以及为什么您不应该在没有适当的转义以防止XSS攻击的情况下不应该在form action属性中使用它。

Pfft。那么,在不使用进行转义的情况下,您不应在任何属性中使用任何内容,因此那里的服务器变量没有什么特别的。htmlspecialchars($string, ENT_QUOTES)


坚持使用解决方案(a),(b)并不是真的安全,因为在HTTP请求中使用绝对URI允许基于名称的虚拟主机安全绕过。因此,真正的规则是永远不要信任SERVER_NAME或HTTP_HOST。
regilero 2014年

@bobince,上述搜索引擎劫持如何工作?搜索引擎将单词映射到域名url,而不处理IP。那么,为什么说“攻击者可以使搜索引擎attacker.com成为服务器IP的最佳主要来源”呢?对于搜索引擎而言,这似乎并不意味着什么,那甚至会做什么呢?
Pacerier

2
Google当然具有(并且可能仍具有某种形式)重复站点的概念,因此,如果您的站点可以通过进行访问http://example.com/http://www.example.com/并且http://93.184.216.34/将它们组合成一个站点,则选择最受欢迎的地址,然后仅返回指向该地址的链接版。如果您可以指向evil-example.com相同的地址并让Google简短地将其视为最受欢迎的地址,则可以窃取该网站的内容。我不知道今天这有多实用,但是我已经看到俄罗斯的链接农场攻击者过去曾尝试这样做。
bobince 2015年

24

这是Symfony用于获取主机名的详细翻译(请参阅第二个示例以获取更多的文字翻译):

function getHost() {
    $possibleHostSources = array('HTTP_X_FORWARDED_HOST', 'HTTP_HOST', 'SERVER_NAME', 'SERVER_ADDR');
    $sourceTransformations = array(
        "HTTP_X_FORWARDED_HOST" => function($value) {
            $elements = explode(',', $value);
            return trim(end($elements));
        }
    );
    $host = '';
    foreach ($possibleHostSources as $source)
    {
        if (!empty($host)) break;
        if (empty($_SERVER[$source])) continue;
        $host = $_SERVER[$source];
        if (array_key_exists($source, $sourceTransformations))
        {
            $host = $sourceTransformations[$source]($host);
        } 
    }

    // Remove port number from host
    $host = preg_replace('/:\d+$/', '', $host);

    return trim($host);
}

过时的:

这是我对Symfony框架中使用的方法的裸露的PHP的翻译,该方法尝试按照最佳实践的顺序从所有可能的方式获取主机名:

function get_host() {
    if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])
    {
        $elements = explode(',', $host);

        $host = trim(end($elements));
    }
    else
    {
        if (!$host = $_SERVER['HTTP_HOST'])
        {
            if (!$host = $_SERVER['SERVER_NAME'])
            {
                $host = !empty($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '';
            }
        }
    }

    // Remove port number from host
    $host = preg_replace('/:\d+$/', '', $host);

    return trim($host);
}

1
@StefanNch请定义“这种方式”。
showdev 2014年

1
@showdev我真的很难阅读类似if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])或的条件语句x = a == 1 ? True : False。第一次看到它时,我的大脑正在寻找$ host实例化,并回答“为什么只有一个“ =符号”?我开始不喜欢弱类型的编程语言。一切写法都不一样。您不节省时间,也不特殊。我不会以这种方式编写代码,因为随着时间的流逝,我是需要对其进行调试的人。看起来很乱,大脑很累!我知道我的英语很赞,但至少我会尝试。
StefanNch 2014年

1
伙计们,我只是从Symfony移植了代码。这就是我采取的方式。对于所有问题而言,它都很有效,而且看起来很彻底。我,我自己,这还不够可读,但是我没有时间完全重写它。
抗毒的2014年

2
对我来说很好。这些是三元运算符,如果使用得当,实际上可以节省时间(和字节),而不会降低可读性。
showdev 2014年

1
@ antitoxic,-1 Symfony编码器(与许多其他编码器一样)在这种情况下并不完全知道他们在做什么。这不会为您提供主机名(请参见Simon的答案)。这只是给您最好的猜测,这将是错误的很多次。
Pacerier 2015年

11

$_SERVER['HTTP_HOST']用于站点上的所有链接而不必担心XSS攻击(即使以表格形式使用)是否“安全” ?

是的,它是安全的使用$_SERVER['HTTP_HOST'],(甚至$_GET$_POST只要你确认他们接受他们。这是我为安全生产服务器所做的工作:

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
$reject_request = true;
if(array_key_exists('HTTP_HOST', $_SERVER)){
    $host_name = $_SERVER['HTTP_HOST'];
    // [ need to cater for `host:port` since some "buggy" SAPI(s) have been known to return the port too, see http://goo.gl/bFrbCO
    $strpos = strpos($host_name, ':');
    if($strpos !== false){
        $host_name = substr($host_name, $strpos);
    }
    // ]
    // [ for dynamic verification, replace this chunk with db/file/curl queries
    $reject_request = !array_key_exists($host_name, array(
        'a.com' => null,
        'a.a.com' => null,
        'b.com' => null,
        'b.b.com' => null
    ));
    // ]
}
if($reject_request){
    // log errors
    // display errors (optional)
    exit;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
echo 'Hello World!';
// ...

的优点$_SERVER['HTTP_HOST']是它的行为比更加明确$_SERVER['SERVER_NAME']。对比➫➫

主机的内容:当前请求中的标头(如果有)。

与:

正在执行当前脚本的服务器主机的名称。

使用类似的更好定义的接口 $_SERVER['HTTP_HOST']意味着更多的SAPI将使用可靠的明确定义的行为来实现它。(与另一个不同。)但是,它仍然完全依赖 SAPI :

不能保证每个Web服务器都会提供这些[ $_SERVER条目]中的任何一个;服务器可能会省略某些服务器,或提供此处未列出的其他服务器。

要了解如何正确检索主机名,首先,您需要了解仅包含代码的服务器无法知道网络上自己的名称(验证的前提)。它需要与提供自己名称的组件进行接口。这可以通过以下方式完成:

  • 本地配置文件

  • 本地数据库

  • 硬编码的源代码

  • 外部要求(卷曲

  • 客户/攻击者的Host:请求

  • 等等

通常,它是通过本地(SAPI)配置文件完成的。请注意,您已经正确配置了它,例如在Apache➫➫中

为了使动态虚拟主机看起来像普通主机,需要“伪造”几件事。

最重要的是服务器名称,Apache使用该服务器名称来生成自引用URL等。它使用ServerName指令进行配置,并且可以通过SERVER_NAME环境变量供CGI使用。

运行时使用的实际值 UseCanonicalName设置控制

随着 UseCanonicalName Off服务器的名字来自内容Host:的请求头。有了 UseCanonicalName DNS它来自虚拟主机的IP地址的反向DNS查找。前一种设置用于基于名称的动态虚拟托管,而后一种设置用于基于IP的**托管。

如果 Apache因为没有Host:头而无法计算出服务器名称,或者DNS查找失败,那么ServerName使用配置的值代替。


8

两者之间的主要区别$_SERVER['SERVER_NAME']是服务器控制的变量,而$_SERVER['HTTP_HOST']用户控制的值。

经验法则是永远不要信任用户的值,因此$_SERVER['SERVER_NAME']是更好的选择。

正如Gumbo指出的那样,如果您未设置,Apache将从用户提供的值构造SERVER_NAME UseCanonicalName On

编辑:说了这么多,如果该站点使用基于名称的虚拟主机,则HTTP Host标头是访问非默认站点的唯一方法。


明白了 我的困扰是“用户如何更改$ _SERVER ['HTTP_HOST']的值?” 可能吗
Jeff

5
用户可以更改它,因为它只是传入请求中Host头的内容。主服务器(或绑定到默认主机:80 的VirtualHost )将响应所有未知主机,因此该站点上Host标记的内容可以设置为任何内容。
Powerlord

4
请注意,基于IP的虚拟主机将始终对它们的特定IP做出响应,因此您在任何情况下都不能信任它们上的HTTP主机值。
Powerlord

1
@Jeff,这就像在问:“可以打电话给披萨小屋的电​​话号码,并要求与肯德基的工作人员通话吗?” 当然可以,您可以要求任何东西。@Powerlord,这与基于IP的虚拟主机无关。您的服务器,无论是否基于IP的虚拟主机,在任何情况下都不能信任HTTP的Host:值,除非您已经通过手动或通过SAPI的设置对其进行了验证
Pacerier'3

3

我不确定也不真正信任,$_SERVER['HTTP_HOST']因为它取决于客户端的标头。换句话说,如果客户端请求的域不是我的域,则它们将不会进入我的站点,因为DNS和TCP / IP协议将其指向正确的目的地。但是我不知道是否有可能劫持DNS,网络甚至Apache服务器。为了安全起见,我在环境中定义了主机名,并将其与进行比较$_SERVER['HTTP_HOST']

SetEnv MyHost domain.com在根上添加.htaccess文件,并在Common.php中添加此代码

if (getenv('MyHost')!=$_SERVER['HTTP_HOST']) {
  header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request');
  exit();
}

我在每个php页面中都包含这个Common.php文件。该页面会执行每个请求所需的任何操作,例如session_start(),修改会话cookie并拒绝post方法是否来自其他域。


1
当然可以绕过DNS。攻击者可以简单地Host:直接向服务器的IP 发出欺诈值。
Pacerier,2015年


1

首先,我要感谢您提供的所有良好答案和解释。这是我根据所有答案创建的方法,用于获取基本url。我仅在非常罕见的情况下使用它。因此,没有像XSS攻击那样将重点放在安全性问题上。也许有人需要它。

// Get base url
function getBaseUrl($array=false) {
    $protocol = "";
    $host = "";
    $port = "";
    $dir = "";  

    // Get protocol
    if(array_key_exists("HTTPS", $_SERVER) && $_SERVER["HTTPS"] != "") {
        if($_SERVER["HTTPS"] == "on") { $protocol = "https"; }
        else { $protocol = "http"; }
    } elseif(array_key_exists("REQUEST_SCHEME", $_SERVER) && $_SERVER["REQUEST_SCHEME"] != "") { $protocol = $_SERVER["REQUEST_SCHEME"]; }

    // Get host
    if(array_key_exists("HTTP_X_FORWARDED_HOST", $_SERVER) && $_SERVER["HTTP_X_FORWARDED_HOST"] != "") { $host = trim(end(explode(',', $_SERVER["HTTP_X_FORWARDED_HOST"]))); }
    elseif(array_key_exists("SERVER_NAME", $_SERVER) && $_SERVER["SERVER_NAME"] != "") { $host = $_SERVER["SERVER_NAME"]; }
    elseif(array_key_exists("HTTP_HOST", $_SERVER) && $_SERVER["HTTP_HOST"] != "") { $host = $_SERVER["HTTP_HOST"]; }
    elseif(array_key_exists("SERVER_ADDR", $_SERVER) && $_SERVER["SERVER_ADDR"] != "") { $host = $_SERVER["SERVER_ADDR"]; }
    //elseif(array_key_exists("SSL_TLS_SNI", $_SERVER) && $_SERVER["SSL_TLS_SNI"] != "") { $host = $_SERVER["SSL_TLS_SNI"]; }

    // Get port
    if(array_key_exists("SERVER_PORT", $_SERVER) && $_SERVER["SERVER_PORT"] != "") { $port = $_SERVER["SERVER_PORT"]; }
    elseif(stripos($host, ":") !== false) { $port = substr($host, (stripos($host, ":")+1)); }
    // Remove port from host
    $host = preg_replace("/:\d+$/", "", $host);

    // Get dir
    if(array_key_exists("SCRIPT_NAME", $_SERVER) && $_SERVER["SCRIPT_NAME"] != "") { $dir = $_SERVER["SCRIPT_NAME"]; }
    elseif(array_key_exists("PHP_SELF", $_SERVER) && $_SERVER["PHP_SELF"] != "") { $dir = $_SERVER["PHP_SELF"]; }
    elseif(array_key_exists("REQUEST_URI", $_SERVER) && $_SERVER["REQUEST_URI"] != "") { $dir = $_SERVER["REQUEST_URI"]; }
    // Shorten to main dir
    if(stripos($dir, "/") !== false) { $dir = substr($dir, 0, (strripos($dir, "/")+1)); }

    // Create return value
    if(!$array) {
        if($port == "80" || $port == "443" || $port == "") { $port = ""; }
        else { $port = ":".$port; } 
        return htmlspecialchars($protocol."://".$host.$port.$dir, ENT_QUOTES); 
    } else { return ["protocol" => $protocol, "host" => $host, "port" => $port, "dir" => $dir]; }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.