PHP $_ SERVER [‘ HTTP _ HOST’] vs. $_ SERVER [‘ SERVER _ NAME’] ,我对手册页的理解正确吗?

我做了大量的搜索,还阅读了 PHP $_ SERVER docs。我有这个权利,关于我的 PHP 脚本使用简单的链接定义在我的整个网站使用?

$_SERVER['SERVER_NAME']基于 Web 服务器的配置文件(在我的例子中是 Apache2) ,并且根据一些指令而变化: (1) VirtualHost,(2) ServerName,(3) UseCanonicalName,等等。

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

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

后续评论:

读完这篇文章后,我想我有点多疑了,因为我注意到有些人说“他们不会相信任何 $_SERVER变量”:

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

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

如果我说错了,请纠正我。

492122 次浏览

这可能是每个人的第一个想法。但它有点困难。参见 Chris Shiflett 的文章《 ABC0对抗 HTTP_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;
}

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

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

正如 Gumbo 指出的,如果不设置 UseCanonicalName On,Apache 将根据用户提供的值构造 SERVER _ NAME。

编辑: 说了这么多,如果站点使用基于名称的虚拟主机,HTTP 主机头是到达不是默认站点的站点的唯一方法。

都可以。它们都是同样(在)安全的,因为在许多情况下,SERVER _ NAME 只是从 HTTP _ HOST 中填充的。我通常使用 HTTP _ HOST,这样用户就可以保留他们开始使用的主机名。例如,如果我在。和。Org 域名,我不想派人从。组织。Com,特别是如果他们可能有登录令牌。组织,他们会失去,如果发送到另一个域名。

无论哪种方式,你只需要确保你的网络应用程序将永远只会响应已知的好域名。这可以通过(a)像 Gumbo 那样的应用程序端检查来完成,或者(b)通过在域名上使用虚拟主机来处理给出未知主机头的请求。

这样做的原因是,如果你允许用任何旧的名字访问你的网站,你就使自己处于 DNS 重新绑定攻击的开放状态(另一个网站的主机名指向你的 IP,一个用户用攻击者的主机名访问你的网站,然后主机名被移动到攻击者的 IP,带走你的 cookies/auth)和搜索引擎劫持(攻击者指向他们自己的主机名在你的网站,并试图让搜索引擎看到它是“最好的”主机名)。

显然,讨论的主要内容是 $_ SERVER [‘ PHP _ SELF’] ,以及为什么不应该在 form action 属性中使用它,而不进行适当的转义以防止 XSS 攻击。

噗。在 任何属性中不应该使用 什么都行而不用 htmlspecialchars($string, ENT_QUOTES)转义,所以这里没有关于服务器变量的特殊之处。

我不确定也不真正信任 $_SERVER['HTTP_HOST'],因为它取决于来自客户端的消息头。另一方面,如果客户端请求的域不是我的域,他们将不会进入我的网站,因为 DNS 和 TCP/IP 协议指向正确的目的地。然而,我不知道是否有可能劫持的 DNS,网络,甚至阿帕奇服务器。为了安全起见,我在环境中定义了主机名,并将其与 $_SERVER['HTTP_HOST']进行了比较。

在 root 上的. htaccess 文件中添加 SetEnv MyHost domain.com,并在 Common.php 中添加 ths 代码

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

我在每个 php 页面中都包含这个 Common.php 文件。该页面为每个请求(如 session_start())做任何需要的事情,修改会话 cookie,如果 post 方法来自不同的域,则拒绝。

这是 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 框架中使用的一种方法的解释,这种方法试图通过各种可能的方式获得主机名,以便最佳实践:

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);
}

只是一个附加说明-如果服务器运行在80以外的端口上(在开发/内部网计算机上可能很常见) ,那么 HTTP_HOST包含该端口,而 SERVER_NAME不包含该端口。

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

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

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

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

对站点上的所有链接使用 $_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']更加明确:

Host.header 的内容(如果有的话)来自当前请求。

与:

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

使用定义更好的接口(如 $_SERVER['HTTP_HOST'])意味着更多的 SAPI 将使用定义良好的 可靠行为来实现它。(与 另一个不同。)然而,它仍然完全依赖于 SAPI 的 Something something:

不能保证每个 Web 服务器都会提供这些[ $_SERVER条目] ; 服务器可能会省略一些条目,或者提供这里没有列出的其他条目。

要理解如何正确地检索主机名,首先需要理解的是,只包含 密码的服务器无法知道(验证的先决条件)网络上的 它自己的名字。它需要与提供其自身名称的组件进行接口。这可以通过:

  • 本地配置文件本地配置文件

  • 本地数据库

  • 硬编码的源代码

  • 外部请求(卷发)

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

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

为了让动态虚拟主机看起来像一个正常的主机,需要“伪造”一些东西。

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

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

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

如果 Apache 不能计算出服务器名称,因为没有 Host:头或 DNS 查找失败 那么,则使用用 ServerName配置的值。

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