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

Posted

技术标签:

【中文标题】PHP $_SERVER[\'HTTP_HOST\'] vs. $_SERVER[\'SERVER_NAME\'],我对手册页的理解正确吗?【英文标题】:PHP $_SERVER['HTTP_HOST'] vs. $_SERVER['SERVER_NAME'], am I understanding the man pages correctly?PHP $_SERVER['HTTP_HOST'] vs. $_SERVER['SERVER_NAME'],我对手册页的理解正确吗? 【发布时间】:2010-11-30 09:24:37 【问题描述】:

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

$_SERVER['SERVER_NAME'] 基于您的 Web 服务器的配置文件(在我的例子中是 Apache2),并且取决于几个指令:(1) VirtualHost、(2) ServerName、(3) UseCanonicalName 等。

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

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

跟进 cmets:

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

http://markjaquith.wordpress.com/2009/09/21/php-server-vars-not-safe-in-forms-or-links/

http://php.net/manual/en/reserved.variables.server.php#89567(评论:Vladimir Kornea 14-Mar-2009 01:06)

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

我对上述原始问题的结论是,对网站上的所有链接使用$_SERVER['HTTP_HOST'] 是“安全的”,而不必担心 XSS 攻击,即使在表单中使用也是如此。

如果我错了,请纠正我。

【问题讨论】:

【参考方案1】:

这可能是每个人的第一个想法。但这有点困难。见Chris Shiflett’s article SERVER_NAME Versus HTTP_HOST

似乎没有灵丹妙药。只有当您force Apache to use the canonical name 时,您才能始终使用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;

【讨论】:

大声笑,我读了那篇文章,但它似乎并没有真正回答我的问题。专业开发者使用哪一个?如果有的话。 有趣的是,我从来不知道 SERVER_NAME 在 Apache 中默认使用用户提供的值。 @Jeff,对于托管多个子/域的服务器,您只有两个选择$_SERVER['SERVER_NAME']$_SERVER['HTTP_HOST'](除了根据用户请求实现一些其他自定义握手)。专业开发者不相信他们不完全理解的东西。所以他们要么有他们的 SAPI 设置完全正确(在这种情况下他们使用的选项给出正确的结果),或者他们会做白名单,这样它就不会不管 SAPI 提供什么价值。 @Gumbo,由于某些 SAPI 存在严重问题,您需要 apply the "port" patch。此外,array_key_exists 是 more scalable,而 in_array 的性能为 O(n)。 @Pacerier array_key_exists 和 in_array 做不同的事情,前者检查键,后者检查值,所以你不能只是交换它们。此外,如果您有一个包含两个值的数组,则不必担心 O(n) 性能...【参考方案2】:

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

经验法则是永远不要相信来自用户的值,所以$_SERVER['SERVER_NAME'] 是更好的选择。

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

编辑:话虽如此,如果站点使用基于名称的虚拟主机,HTTP Host 标头是访问不是默认站点的站点的唯一方法。

【讨论】:

明白。我的挂断是“用户如何更改 $_SERVER['HTTP_HOST'] 的值?”有可能吗? 用户可以更改它,因为它只是来自传入请求的 Host 标头的内容。主服务器(或绑定到 default:80 的 VirtualHost)将响应所有未知主机,因此该站点上的 Host 标签的内容可以设置为任何内容。 请注意,基于 IP 的虚拟主机将始终响应其特定 IP,因此您不能在任何情况下信任它们的 HTTP Host 值。 @Jeff,这就像问“可以拨打必胜客的电话号码并请求与肯德基员工通话吗?”当然,您可以请求任何您想要的。 @Powerlord,这与基于 IP 的虚拟主机无关。无论是否基于 IP 的虚拟主机,您的服务器在任何情况下都无法信任 HTTP 的 Host: 值,除非您手动或通过 SAPI 设置已经验证它。【参考方案3】:

使用任何一个。它们都同样(不)安全,因为在许多情况下 SERVER_NAME 只是从 HTTP_HOST 填充。我通常选择 HTTP_HOST,这样用户就可以使用他们开始时使用的确切主机名。例如,如果我在 .com 和 .org 域上拥有相同的站点,我不想将某人从 .org 发送到 .com,特别是如果他们可能在 .org 上有登录令牌,如果发送到他们会丢失另一个域。

无论哪种方式,您只需要确保您的 web 应用程序只会响应已知良好的域。这可以通过 (a) 使用像 Gumbo 那样的应用程序端检查来完成,或者 (b) 在您希望 不响应 的请求的域名上使用虚拟主机一个未知的主机头。

这样做的原因是,如果您允许以任何旧名称访问您的网站,您就会面临 DNS 重新绑定攻击(另一个网站的主机名指向您的 IP,用户使用攻击者的主机名访问您的网站,然后将主机名移动到攻击者的 IP,并带上您的 cookie/auth)和搜索引擎劫持(攻击者将自己的主机名指向您的站点并试图让搜索引擎将其视为“最佳”主主机名)。

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

噗。好吧,你不应该在 any 属性中使用 anything 而不使用 htmlspecialchars($string, ENT_QUOTES) 转义,所以那里的服务器变量没有什么特别之处。

【讨论】:

继续使用解决方案 (a)、(b) 并不安全,在 HTTP 请求中使用绝对 URI 可以绕过基于名称的虚拟主机安全。所以真正的规则是从不信任 SERVER_NAME 或 HTTP_HOST。 @bobince,上面提到的搜索引擎劫持是如何工作的?搜索引擎将单词映射到域 urls,它们不处理 IP。那么为什么说“攻击者可以让搜索引擎将attacker.com 视为您服务器IP 的最佳主要来源”是什么意思呢?这对搜索引擎来说似乎没有任何意义,那还能做什么? 谷歌当然有(并且可能仍然以某种形式)欺骗网站的概念,所以如果你的网站可以通过http://example.com/http://www.example.com/http://93.184.216.34/访问,它会将它们组合成一个站点,选择最受欢迎的地址,然后只返回指向该版本的链接。如果您可以将evil-example.com 指向同一个地址并让Google 简要地将其视为更受欢迎的地址,您就可以窃取该网站的流量。我不知道这在今天有多实用,但我看到俄罗斯链接农场攻击者过去曾尝试这样做。【参考方案4】:

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

在根目录的.htaccess文件中添加SetEnv MyHost domain.com并在Common.php中添加代码

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

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

【讨论】:

当然可以绕过DNS。攻击者可以直接向您的服务器 IP 发出一个欺骗性的 Host: 值。【参考方案5】:

这是对 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);

【讨论】:

@StefanNch 请定义“这种方式”。 @showdev 我真的觉得“很难”阅读像if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])x = a == 1 ? True : False 这样的条件语句。我第一次看到它时,我的大脑正在寻找 $host 实例化和“为什么只有一个“=”符号?”的答案。我开始不喜欢弱类型编程语言。一切都写得不一样。你不节省时间,你并不特别。我不会这样写代码,因为随着时间的推移,我才是需要调试它的人。对于疲惫的大脑来说,看起来真的很乱!我知道我的英语是engrish,但至少我尝试过。 伙计们,我只是从 Symfony 移植了代码。这是我采取的方式。不管怎样,它很有效,而且看起来很彻底。我自己也觉得这不够可读,但我还没有时间完全重写它。 对我来说看起来不错。这些是ternary operators,如果使用得当,实际上可以在不降低可读性的情况下节省时间(和字节)。 @antitoxic, -1 Symfony 编码员(像许多其他人一样)并不完全知道他们在这种情况下在做什么。这不会给你主机名(见西蒙的回答)。这只是给你一个最好的猜测,这会是错很多次。【参考方案6】:

补充一点 - 如果服务器在 80 以外的端口上运行(这在开发/内联网机器上可能很常见),那么 HTTP_HOST 包含该端口,而 SERVER_NAME 不包含。

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

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

正如 Mike 在下面指出的那样,HTTP_HOST 在 HTTPS 上运行时包含:443(除非您在非标准端口上运行,我还没有测试过)。

【讨论】:

注意:443 的 HTTP_HOST 中也不存在该端口(默认 SSL 端口)。 换句话说,HTTP_HOST 的值并不完全是用户提供的Host: 参数。它只是基于此。 @Pacerier 不,恰恰相反:HTTP_HOST 正是随 HTTP 请求提供的 Host: 字段。端口是其中的一部分,当它是默认端口时浏览器不会提及它(HTTP 为 80;HTTPS 为 443)【参考方案7】:

XSS 将始终存在,即使您使用 $_SERVER['HTTP_HOST']$_SERVER['SERVER_NAME']$_SERVER['PHP_SELF']

【讨论】:

【参考方案8】:

对网站上的所有链接使用$_SERVER['HTTP_HOST'] 是否“安全”而不必担心 XSS 攻击,即使在表单中使用也是如此?

是的,使用$_SERVER['HTTP_HOST'] 是safe,(甚至$_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'] 更明确。对比➫➫:

Host 的内容:当前请求的标头,如果有的话。

与:

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

使用像$_SERVER['HTTP_HOST'] 这样定义更好的接口意味着更多的SAPI 将使用可靠 定义良好的行为来实现它。 (不像the other。)但是,它仍然完全依赖于SAPI ➫➫:

无法保证每个网络服务器都会提供这些 [$_SERVER 条目];服务器可能会省略一些,或提供此处未列出的其他内容。

要了解如何正确检索主机名,首先您需要了解仅包含代码的服务器无法知道(验证的先决条件)它在网络上拥有自己的名字。它需要与为其提供自己名称的组件交互。这可以通过以下方式完成:

本地配置文件

本地数据库

硬编码源代码

外部请求 (curl)

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

通常通过本地 (SAPI) 配置文件完成。请注意,您已正确配置它,例如在阿帕奇➫➫:

需要“伪造”一些东西才能使动态虚拟主机看起来像一个正常的主机。

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

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

使用 UseCanonicalName Off 服务器名称来自请求中Host: 标头的内容。 使用 UseCanonicalName DNS 它来自虚拟主机 IP 地址的反向 DNS 查找。前者用于基于名称的动态虚拟主机,后者用于**基于IP的主机。

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

【讨论】:

我建议使用isset 而不是array_key_exists(总是)。 isset 是一种语言结构,而 array_key_exists 执行数组所有元素的循环。这可能是一个很小的开销,但除非有理由使用较慢的过程,否则我认为最好避免。不过,我总是对改进感兴趣。 @AndyGee array_key_exists 是哈希查找而不是循环,就像isset。我想你在想in_array @Anther 是的,你是对的,他们都做哈希查找,谢谢。尽管isset 作为一种语言结构,但要遍历的代码更少,因此速度明显更快。我觉得现在这有点离题了,答案没有错 - 更多的是一个普遍的共识点。【参考方案9】:

首先,我要感谢您提供的所有好的答案和解释。 这是我根据您获取基本网址的所有答案创建的方法。我只在非常罕见的情况下使用它。因此,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]; 

【讨论】:

以上是关于PHP $_SERVER['HTTP_HOST'] vs. $_SERVER['SERVER_NAME'],我对手册页的理解正确吗?的主要内容,如果未能解决你的问题,请参考以下文章

JSP 或 JavaScript 等价于 PHP 的 $_SERVER["HTTP_HOST"]?

php 根据环境$ _SERVER ['HTTP_HOST']有条件地将配置文件加载到wp_config中

php 获取域名

是否可以更改 htaccess 中的 $_SERVER['HTTP_HOST'] ? [关闭]

$_SERVER['HTTP_HOST'] 也包含端口号 =/

php$_SERVER常用用法