PHP $ _SERVER ['HTTP_HOST'] против $ _SERVER ['SERVER_NAME'], правильно ли я понимаю справочные страницы?

167

Я много искал, а также читал документацию по PHP $ _SERVER . Имею ли я это право в отношении того, какие скрипты PHP использовать для простых определений ссылок, используемых на моем сайте?

$_SERVER['SERVER_NAME'] основан на файле конфигурации вашего веб-сервера (в моем случае Apache2) и варьируется в зависимости от нескольких директив: (1) VirtualHost, (2) ServerName, (3) UseCanonicalName и т. д.

$_SERVER['HTTP_HOST'] основано на запросе от клиента.

Поэтому мне кажется, что будет правильным использовать его, чтобы сделать мои сценарии максимально совместимыми $_SERVER['HTTP_HOST']. Это предположение верно?

Последующие комментарии:

Полагаю, я немного заразился после прочтения этой статьи и заметил, что некоторые люди говорят: «Они не будут доверять ни одной из $_SERVERперемен»:

Очевидно, речь идет в основном о том, $_SERVER['PHP_SELF']почему вы не должны использовать его в атрибуте действия формы без надлежащего экранирования для предотвращения атак XSS.

Мой вывод о моем первоначальном вопросе выше состоит в том, что «безопасно» использовать $_SERVER['HTTP_HOST']все ссылки на сайте, не беспокоясь о XSS-атаках, даже если они используются в формах.

Пожалуйста, поправьте меня, если я ошибаюсь.

Джефф
источник

Ответы:

149

Наверное, это первая мысль каждого. Но это немного сложнее. См. Статью Криса Шифлетта 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
Лол, я прочитал эту статью, и она, похоже, не отвечала на мой вопрос. Какой из них используют профессиональные разработчики? Если либо.
Джефф
2
Интересно, я никогда не знал, что SERVER_NAME использует введенные пользователем значения по умолчанию в Apache.
Powerlord
1
@Jeff, для серверов, на которых размещено более одного субдомена, у вас есть только два варианта $_SERVER['SERVER_NAME']и $_SERVER['HTTP_HOST'](кроме реализации некоторого другого пользовательского рукопожатия, основанного на запросе пользователя). Профессиональные разработчики не доверяют вещам, которые они не понимают полностью. Таким образом, они либо имеют свои настройки SAPI совершенно правильно (в этом случае выбранный ими вариант даст правильный результат), либо они составят белый список так, что не имеет значения, какие значения поставляются SAPI.
Pacerier
@ Gumbo, вам нужно применить патч "port" из-за серьезных проблем с некоторыми SAPI. Кроме того, array_key_existsявляется более масштабируемым по сравнению с тем, in_arrayчто имеет производительность O (n).
Pacerier
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 по умолчанию).
Майк
Таким образом, другими словами, значение HTTP_HOSTне совсем Host:параметр, предоставленный пользователем. Это просто основано на этом.
Pacerier
1
@Pacerier Нет, это наоборот: HTTP_HOST - это именно то поле Host:, которое было предоставлено с HTTP-запросом. Порт является его частью, и браузеры не упоминают его, когда он используется по умолчанию (80 для HTTP; 443 для HTTPS)
xhienne
29

Используйте либо. Они оба одинаково (небезопасны), так как во многих случаях SERVER_NAME в любом случае просто заполняется из HTTP_HOST. Обычно я использую HTTP_HOST, чтобы пользователь оставался на том же имени хоста, на котором он начал. Например, если у меня есть один и тот же сайт в домене .com и .org, я не хочу отправлять кого-то из .org в .com, особенно если у них могут быть токены входа в систему .org, которые они потеряли бы, если бы их отправили в другой домен.

В любом случае, вам просто нужно быть уверенным, что ваше веб-приложение будет отвечать только за известные домены. Это можно сделать либо (а) с помощью проверки на стороне приложения, такой как Gumbo, либо (б) с использованием виртуального хоста на доменных именах, которые вы хотите, которые не отвечают на запросы, которые дают неизвестный заголовок хоста.

Причина этого заключается в том, что если вы разрешаете доступ к вашему сайту под любым старым именем, вы открываете себя для атак повторного связывания DNS (когда имя хоста другого сайта указывает на ваш IP, пользователь получает доступ к вашему сайту с именем хоста злоумышленника, а затем именем хоста). перемещается по IP-адресу злоумышленника, забирая ваши куки / авторизацию с ним) и захватывая поисковую систему (когда злоумышленник указывает свое собственное имя хоста на ваш сайт и пытается заставить поисковые системы видеть его как «лучшее» основное имя хоста).

Очевидно, что речь идет в основном о $ _SERVER ['PHP_SELF'] и о том, почему вы не должны использовать его в атрибуте действия формы без надлежащего экранирования для предотвращения атак XSS.

Пфф. Что ж, вы не должны использовать что-либо в любом атрибуте без экранирования htmlspecialchars($string, ENT_QUOTES), так что там нет ничего особенного в серверных переменных.

bobince
источник
Оставаться с решением (a), (b) на самом деле небезопасно, использование абсолютного URI в HTTP-запросах позволяет обходить защиту виртуальных хостов на основе имен. Поэтому настоящим правилом никогда не является доверие к SERVER_NAME или HTTP_HOST.
Regilero
@bobince, как работает упомянутый поисковик угон? Поисковые системы сопоставляют слова с URL-адресами доменов , они не имеют дело с IP-адресами. Так почему же вы говорите, что «злоумышленник может заставить поисковые системы рассматривать attacker.comлучший первичный источник IP вашего сервера»? Похоже, это ничего не значит для поисковых систем. Что это вообще собирается делать?
Пейсер
2
Google, безусловно, имел (и, вероятно, все еще имеет в некоторой форме) концепцию двойных сайтов, поэтому, если ваш сайт доступен как http://example.com/, http://www.example.com/и http://93.184.216.34/он будет объединять их в один сайт, выберите самый популярный из адресов и верните только ссылки на него. версия. Если бы вы могли указать evil-example.comна тот же адрес и сделать так, чтобы Google кратко заметил, что в качестве более популярного адреса вы могли бы украсть сок сайта. Я не знаю, насколько это практично сегодня, но я видел, как российские злоумышленники пытались сделать это в прошлом.
бобинце
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);
}

Устаревшие:

Это мой перевод на голый PHP метода, используемого в платформе 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);
}
противоядный
источник
1
@StefanNch Пожалуйста, определите «этот путь».
showdev
1
@showdev Я действительно нахожу "трудно" читать оператор условия вроде if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])или x = a == 1 ? True : False. В первый раз, когда я увидел это, мой мозг искал инстанцирования $ host и ответа на вопрос «почему только один» = «знак?». Я начинаю не любить слабые языки программирования. Все написано по-другому. Вы не экономите время, и вы не особенный. Я не пишу код таким образом, потому что по прошествии времени я должен отлаживать его. Выглядит очень грязно для усталого мозга! Я знаю, что мой английский - английский, но, по крайней мере, я стараюсь.
StefanNch
1
ребята, я просто портировал код от Symfony. Я так и принял. Для всего это имеет значение - это работает и кажется довольно основательным. Я тоже считаю, что это недостаточно читабельно, но у меня не было времени переписать его полностью.
антитоксический
2
Выглядит хорошо для меня. Это троичные операторы, которые могут сэкономить время (и байты) без потери читабельности при правильном использовании.
showdev
1
@antitoxic, -1 Программисты Symfony (как и многие другие) точно не знают, что они делают в этом случае. Это не дает вам имя хоста (см. Ответ Саймона). Это просто даст вам лучшее предположение, которое много раз будет ошибочным.
Pacerier
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']. Контраст ➫➫ :

Содержимое заголовка Host: из текущего запроса, если таковой имеется.

с участием:

Имя хоста сервера, под которым выполняется текущий скрипт.

Использование более определенного интерфейса, такого как, $_SERVER['HTTP_HOST']означает, что большее количество SAPI будет реализовывать его, используя надежное, четко определенное поведение. (В отличие от других .) Однако он все еще полностью зависит от SAPI ➫➫ :

Нет никакой гарантии, что каждый веб-сервер предоставит любую из этих [ $_SERVERзаписей]; серверы могут пропустить некоторые или предоставить другие, не перечисленные здесь.

Чтобы понять, как правильно получить имя хоста, прежде всего вам необходимо понять, что сервер, который содержит только код, не имеет возможности узнать (предварительное условие для проверки) свое собственное имя в сети. Он должен взаимодействовать с компонентом, который предоставляет ему свое имя. Это можно сделать через:

  • локальный конфигурационный файл

  • локальная база данных

  • жестко закодированный исходный код

  • внешний запрос ( curl )

  • Host:запрос клиента / злоумышленника

  • и т.д

Обычно это делается через локальный (SAPI) файл конфигурации. Обратите внимание, что вы настроили его правильно, например, в Apache ➫➫ :

Несколько вещей нужно «подделать», чтобы динамический виртуальный хост выглядел как обычный.

Наиболее важным является имя сервера, которое используется Apache для генерации URL-адресов, на которые ServerNameимеются ссылки, и т. Д. Оно настраивается с помощью директивы и доступно для CGI через SERVER_NAMEпеременную среды.

Фактическое значение, используемое во время выполнения, контролируется параметром UseCanonicalName.

При UseCanonicalName Off этом имя сервера исходит из содержимого Host:заголовка в запросе. С UseCanonicalName DNS ним происходит от обратного DNS поиска по IP - адресу виртуального хоста. Первый параметр используется для динамического виртуального хостинга на основе имен, а второй - для ** хостинга на основе IP.

Если Apache не может работать имя сервера , потому что нет Host:заголовка или DNS поиск неудачен , то значение , заданное с ServerNameиспользуется вместо этого.

Pacerier
источник
8

Основное различие между ними заключается в том, что $_SERVER['SERVER_NAME']это переменная, управляемая сервером, а $_SERVER['HTTP_HOST']значение, контролируемое пользователем.

Основное правило - никогда не доверять значениям пользователя, поэтому $_SERVER['SERVER_NAME']это лучший выбор.

Как указал Гамбо, Apache создаст SERVER_NAME из предоставленных пользователем значений, если вы не установите их UseCanonicalName On.

Изменить: Сказав все это, если сайт использует виртуальный хост на основе имени, заголовок HTTP Host является единственным способом добраться до сайтов, которые не являются сайтом по умолчанию.

Powerlord
источник
Понял. Мое зависание "как пользователь может изменить значение $ _SERVER ['HTTP_HOST']?" Это вообще возможно?
Джефф
5
Пользователь может изменить это, потому что это только содержимое заголовка Host из входящего запроса. Главный сервер (или VirtualHost, связанный с значением по умолчанию : 80) будет отвечать на все неизвестные хосты, таким образом, содержимое тега Host на этом сайте может быть установлено на что угодно.
Powerlord
4
Обратите внимание, что виртуальные хосты на основе IP ВСЕГДА будут отвечать на их конкретные IP-адреса, поэтому вы ни при каких обстоятельствах не можете доверять им значение хоста HTTP.
Powerlord
1
@Jeff, это все равно что спросить "Можно позвонить по номеру телефона пиццерии и попросить поговорить с персоналом KFC?" Конечно, вы можете запросить все, что вы хотите. @Powerlord, это не имеет ничего общего с виртуальными хостами на базе IP. Ваш сервер, независимо от того, виртуальный хост на основе IP или нет, ни при каких обстоятельствах не может доверять Host:значению HTTP, если вы уже не проверили его, ни вручную, ни через настройку SAPI.
Pacerier
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();
}

Я включаю этот файл Common.php в каждую страницу php. Эта страница делает все, что требуется для каждого запроса, например session_start(), изменяет сеансовый cookie и отклоняет, если метод post пришел из другого домена.

CallMeLaNN
источник
1
Конечно, можно обойти DNS. Злоумышленник может просто ввести мошенническое Host:значение непосредственно в IP-адрес вашего сервера.
Pacerier
1

XSSвсегда будет там, даже если вы используете $_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']или$_SERVER['PHP_SELF']

Джейдип Дэйв
источник
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]; }
}
Майк
источник