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

PHP $_SERVER['HTTP_HOST'] vs. $_SERVER['SERVER_NAME'], am I understanding the manual pages correctly?

提问人:Jeff 提问时间:9/22/2009 最后编辑:MartinJeff 更新时间:5/11/2023 访问量:518716

问:

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

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

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

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

后续评论:

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

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

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

如果我错了,请纠正我。

php apache 安全 owasp

评论


答:

169赞 Gumbo 9/22/2009 #1

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

评论

4赞 Jeff 9/22/2009
哈哈,我读了那篇文章,它似乎并没有真正回答我的问题。专业开发人员使用哪一个?如果任一。
2赞 Powerlord 9/22/2009
有趣的是,我从来不知道SERVER_NAME在 Apache 中默认使用用户提供的值。
1赞 Pacerier 3/6/2015
@Jeff,对于托管多个子/域的服务器,您只有两种选择(除了根据用户请求实现其他一些自定义握手)。专业开发人员不相信他们不完全理解的东西。因此,他们要么完全正确地设置了 SAPI(在这种情况下,他们使用的选项将给出正确的结果),要么他们会白名单列入白名单,这样 SAPI 提供的值就无关紧要了。$_SERVER['SERVER_NAME']$_SERVER['HTTP_HOST']
5赞 eis 7/2/2018
@Pacerier array_key_exists 和 in_array 做不同的事情,前者检查键,后者检查值,所以你不能只是交换它们。此外,如果你有一个包含两个值的数组,你不应该真正担心 O(n) 性能......
1赞 Martin Miller 5/12/2022
我知道这是一个老问题,但如果您的目标是使用全局服务器变量,为什么不使用您控制的环境变量来指定它们呢?Dotenv等
8赞 Powerlord 9/22/2009 #2

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

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

正如 Gumbo 所指出的,如果您不设置 SERVER_NAME.UseCanonicalName On

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

评论

0赞 Jeff 9/22/2009
理解。我的挂断是“用户如何改变 _SERVER['HTTP_HOST'] 美元的价值?甚至可能吗?
5赞 Powerlord 9/23/2009
用户可以更改它,因为它只是传入请求中 Host 标头的内容。主服务器(或绑定到 default:80 的 VirtualHost)将响应所有未知主机,因此该站点上 Host 标记的内容可以设置为任何内容。
4赞 Powerlord 9/23/2009
请注意,基于 IP 的虚拟主机将始终在其特定 IP 上做出响应,因此在任何情况下都不能信任其上的 HTTP 主机值。
1赞 Pacerier 3/6/2015
@Jeff,这就像问“可以拨打必胜客的电话号码并要求与肯德基工作人员交谈吗?当然,你可以要求任何你想要的东西。@Powerlord,这与基于 IP 的虚拟主机无关。无论是否基于 IP 的虚拟主机,您的服务器在任何情况下都不能信任 HTTP 的值,除非您已经手动或通过 SAPI 的设置对其进行了验证Host:
33赞 bobince 9/23/2009 #3

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

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

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

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

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

评论

0赞 regilero 6/23/2014
坚持使用解决方案(a),(b)并不真正安全,在HTTP请求中使用绝对URI允许基于名称的虚拟主机安全绕过。因此,真正的规则是永远不要信任SERVER_NAME或HTTP_HOST。
0赞 Pacerier 3/6/2015
@bobince,提到的搜索引擎劫持是如何工作的?搜索引擎将单词映射到域 URL,它们不处理 IP。那么,为什么你说“攻击者可以使搜索引擎被视为你服务器IP的最佳主要来源”是什么意思呢?这对搜索引擎来说似乎没有任何意义,那到底有什么用呢?attacker.com
2赞 bobince 3/6/2015
谷歌当然有(并且可能仍然以某种形式)重复网站的概念,因此,如果您的网站可以作为 访问,并且它会将它们合并为一个网站,请选择最受欢迎的地址,并且只返回指向该版本的链接。如果你能指向同一个地址,让谷歌短暂地看到它是更受欢迎的地址,你就可以窃取该网站的汁液。我不知道这在今天有多实用,但我过去曾看到俄罗斯链接农场攻击者试图这样做。http://example.com/http://www.example.com/http://93.184.216.34/evil-example.com
4赞 CallMeLaNN 3/15/2010 #4

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

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

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

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

评论

2赞 Pacerier 3/6/2015
当然,绕过DNS是可能的。攻击者可以简单地直接向您服务器的 IP 发出一个脆弱的值。Host:
27赞 antitoxic 1/18/2012 #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);
}

评论

1赞 showdev 7/23/2014
@StefanNch 请定义“这种方式”。
3赞 StefanNch 7/23/2014
@showdev我真的觉得“很难”阅读条件语句,例如 或 .我第一次看到它时,我的大脑正在寻找$host实例化和“为什么只有一个”=“符号的答案。我开始不喜欢弱类型编程语言。一切都写得不同。你不会节省时间,你并不特别。我不会以这种方式编写代码,因为时间过去了,我是需要调试它的人。对于疲惫的大脑来说,看起来真的很乱!我知道我的英语是英语,但至少我尝试过。if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])x = a == 1 ? True : False
3赞 antitoxic 7/24/2014
伙计们,我只是从 Symfony 移植了代码。这就是我采取的方式。对于所有重要的事情 - 它有效并且看起来很彻底。我,我自己,也觉得这不够可读,但我还没有时间完全重写它。
4赞 showdev 7/25/2014
对我来说看起来不错。这些是三元运算符,如果使用得当,实际上可以在不降低可读性的情况下节省时间(和字节)。
1赞 Pacerier 3/6/2015
@antitoxic,-1 Symfony程序员(像许多其他人一样)并不确切知道他们在这种情况下在做什么。这不会给你主机名(参见 Simon 的回答)。这只会给你一个最好的猜测,这个猜测错很多次。
92赞 Simon East 8/21/2012 #6

另外需要注意的是,如果服务器运行在 80 以外的端口上(这在开发/Intranet 机器上可能很常见),则包含该端口,而不包含该端口。HTTP_HOSTSERVER_NAME

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

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

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

评论

6赞 Mike 9/12/2013
注意:该端口在 443 的 HTTP_HOST 中也不存在(默认 SSL 端口)。
0赞 Pacerier 3/6/2015
因此,换句话说,的值并不完全是用户提供的参数。它;仅基于此。HTTP_HOSTHost:
4赞 xhienne 8/27/2018
@Pacerier 否,情况恰恰相反:HTTP_HOST 正是随 HTTP 请求提供的 Host: 字段。端口是其中的一部分,当它是默认端口时,浏览器不会提及它(HTTP为80;HTTPS为443)
1赞 Jaydeep Dave 1/10/2014 #7

XSS即使您使用 ,也会一直在那里,或者$_SERVER['HTTP_HOST']$_SERVER['SERVER_NAME']$_SERVER['PHP_SELF']

12赞 Pacerier 3/6/2015 #8

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

是的,只要您在接受它们之前验证它们,就可以安全使用它们(甚至和)。这就是我为安全生产服务器所做的:$_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: 标头的内容(如果有)。

跟:

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

使用定义更好的接口(如)意味着更多的 SAPI 将使用可靠、定义良好的行为来实现它。(与其他不同。但是,它仍然完全依赖于 SAPI ➫➫$_SERVER['HTTP_HOST']

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

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

  • 本地配置文件

  • 本地数据库

  • 硬编码源代码

  • 外部请求 (curl)

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

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

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

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

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

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

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

评论

1赞 Andy Gee 6/23/2021
我建议使用而不是(总是)。isset 是一种语言结构,而 array_key_exists 执行数组所有元素的循环。这可能是一个很小的开销,但除非有理由使用较慢的过程,否则在我看来最好避免。不过,我总是对改进感兴趣。issetarray_key_exists
0赞 Anther 11/12/2021
@AndyGee 是哈希查找而不是循环,就像 .我想你在想.array_key_existsissetin_array
1赞 Andy Gee 11/14/2021
@Anther 是的,你是对的,他们都做了哈希查找,谢谢。不过,作为一种语言结构,要遍历的代码更少,因此速度明显更快。我觉得这现在有点跑题了,答案没有错——实际上更像是一个普遍的共识点。isset
1赞 Mike 2/26/2019 #9

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