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

时间:2009-09-22 12:16:48

标签: php apache security owasp

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

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

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

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

跟进评论

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

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

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

如果我错了,请纠正我。

9 个答案:

答案 0 :(得分:135)

这可能是每个人的第一个想法。但这有点困难。请参阅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;
}

答案 1 :(得分:62)

另外需要注意的是 - 如果服务器在80以外的端口上运行(在开发/内联网机器上可能是常见的),则HTTP_HOST包含端口,而SERVER_NAME则不包含端口。 / p>

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

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

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

答案 2 :(得分: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);
}

答案 3 :(得分:23)

使用其中之一。它们同样(安全),因为在许多情况下,SERVER_NAME只是从HTTP_HOST填充。我通常会使用HTTP_HOST,以便用户保持他们开始的确切主机名。例如,如果我在.com和.org域中拥有相同的站点,我不想将.org中的某人发送到.com,特别是如果他们可能在.org上有登录令牌,那么如果他们被发送到另一个域。

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

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

  

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

Pfft。那么你不应该在任何属性中使用任何而不用htmlspecialchars($string, ENT_QUOTES)转义,所以那里的服务器变量并没有什么特别的。

答案 4 :(得分:8)

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

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

正如Gumbo所指出的,如果你没有设置UseCanonicalName On,Apache将根据用户提供的值构建SERVER_NAME。

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

答案 5 :(得分:8)

  

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

是的,safe使用$_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将使用可靠明确定义的行为来实现它。 (与the other不同。)但是,它仍完全依赖于SAPI ➫➫

  

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

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

  • 本地配置文件

  • 本地数据库

  • 硬编码源代码

  • 外部请求(curl

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

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

  

为了使动态虚拟主机看起来像普通虚拟主机,需要“伪造”一些东西。

     

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

     

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

     

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

     

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

答案 6 :(得分:2)

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

在root上的.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 post方法来自不同的域。

答案 7 :(得分:1)

即使您使用XSS$_SERVER['HTTP_HOST']$_SERVER['SERVER_NAME']

$_SERVER['PHP_SELF']也会一直存在

答案 8 :(得分:0)

首先,我要感谢您提供的所有良好答案和解释。 这是我根据所有答案创建的用于获取基本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]; }
}