PHP $ _SERVER ['HTTP_HOST'] проти $ _SERVER ['SERVER_NAME'], чи правильно я розумію сторінки чоловіка?


167

Я багато шукав, а також читав документи PHP $ _SERVER . Чи маю я таке право стосовно того, яке використання для моїх скриптів PHP для простих визначень посилань, які використовуються на моєму сайті?

$_SERVER['SERVER_NAME'] ґрунтується на конфігураційному файлі вашого веб-сервера (в моєму випадку Apache2) і змінюється залежно від кількох директив: (1) VirtualHost, (2) ім'я сервера, (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, вам потрібно застосувати патч "порт" через серйозні проблеми з певними 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, або (b), використовуючи віртуальний хост у потрібних доменних іменах, які не відповідають на запити, які дають невідомий заголовок хоста.

Причиною цього є те, що якщо ви дозволяєте доступ до вашого сайту під будь-яким старим іменем, ви відкриваєтесь для атаки повторного перезапису DNS (де ім'я хоста іншого сайту вказує на ваш IP-адресу, користувач отримує доступ до вашого сайту з ім'ям хоста нападника, а потім іменем хоста переміщується до IP-адреси зловмисника, приймаючи файли cookie / auth із собою) та викрадання пошукової системи (де зловмисник вказує власне ім’я хоста на вашому сайті та намагається змусити пошукові системи сприймати його як "найкраще" первинне ім'я хоста).

Мабуть, дискусія в основному стосується $ _SERVER ['PHP_SELF'], і чому ви не повинні використовувати його в атрибуті action form без належного втечі, щоб запобігти атакам XSS.

Pfft. Ну, ви не повинні використовувати нічого в жодному атрибуті, не тікаючи з ним htmlspecialchars($string, ENT_QUOTES), тому там немає нічого особливого щодо змінних серверів.


Перебування з рішеннями (a), (b) не дуже безпечно, використання абсолютного URI в HTTP-запитах дозволяє здійснювати байпас безпеки на основі імен на основі імен. Тож справжнє правило є ніколи не довіряйте SERVER_NAME чи HTTP_HOST.
regilero

@bobince, як працює згаданий викрадення пошукової системи? Пошукові системи відображають слова на URL-адреси домену , вони не мають IP-адреси. То чому ви кажете, що "зловмисник може змусити пошукові системи вважати attacker.comнайкращим першоджерелом для IP вашого сервера"? Це, схоже, нічого не означає для пошукових систем. Що це навіть робити?
Pacerier

2
Google, безумовно, мав (і, мабуть, все ще є в якійсь формі) концепцію оманливих сайтів, так що якщо ваш сайт доступний як http://example.com/, http://www.example.com/і http://93.184.216.34/він поєднує їх в один сайт, вибирайте найпопулярніші адреси та повертайте лише посилання на це версія. Якби ти міг вказатиevil-example.com на ту саму адресу і змусити Google коротко побачити, що як більш популярну адресу, ви могли б викрасти сік сайту. Я не знаю, наскільки це практично сьогодні, але я бачив, як російські нападники російських посилань намагалися це зробити раніше.
bobince

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 (як і багато інших) точно не знають, що вони роблять у цьому випадку. Це не дає вам імені хоста (див. Відповідь Саймона). Це просто дає вам найкращу здогадку, яка буде помилятися багато разів.
Печер'є

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']. Контраст ➫➫ :

Зміст хоста: заголовок з поточного запиту, якщо такий є.

з:

Ім'я хоста сервера, під яким виконується поточний сценарій.

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

Немає гарантії, що кожен веб-сервер надасть будь-яку з цих $_SERVERзаписів; сервери можуть опускати деякі або надавати інші, які не вказані тут.

Щоб зрозуміти, як правильно отримати ім'я хоста, перш за все, вам потрібно зрозуміти, що сервер, який містить лише код , не має можливості знати (необхідний для підтвердження) власне ім’я в мережі. Потрібно взаємодіяти з компонентом, який забезпечує його власне ім'я. Це можна зробити за допомогою:

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

  • локальна база даних

  • жорсткий код вихідного коду

  • зовнішній запит ( згортання )

  • Host:запит клієнта / зловмисника

  • тощо

Зазвичай це робиться через локальний (SAPI) конфігураційний файл. Зауважте, що ви правильно налаштували його, наприклад, в Apache ➫➫ :

Пару речей потрібно "підробити", щоб динамічний віртуальний хост виглядав як звичайний.

Найважливішим є ім'я сервера, яке Apache використовується для генерування самореференційних URL-адрес тощо. Це налаштовано ServerNameдирективою, і вона доступна для CGI черезSERVER_NAME змінне оточення.

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

З UseCanonicalName Off ім'ям сервера походить від вмісту Host:заголовка в запиті. З UseCanonicalName DNS його допомогою відбувається зворотний пошук DNS-адреси IP-адреси віртуального хоста. Перший параметр використовується для динамічного віртуального хостингу на основі імен, а останній використовується для ** хостингу на основі IP.

Якщо Apache не може працювати ім'я сервера , тому що немає Host:заголовка або DNS пошук невдалий , то значення , задане з ServerNameвикористовується замість цього.


8

Основна різниця між ними полягає в тому, що $_SERVER['SERVER_NAME']це змінна, керована сервером, а $_SERVER['HTTP_HOST']значення - кероване користувачем.

Основне правило - ніколи не довіряти цінностям користувачеві, так $_SERVER['SERVER_NAME']це кращий вибір.

Як зазначив Gumbo, Apache створить SERVER_NAME з наданих користувачем значень, якщо ви не встановите UseCanonicalName On.

Редагувати: сказавши все це, якщо сайт використовує віртуальний хост на основі імен, заголовок HTTP Host - це єдиний спосіб дістатися до сайтів, які не є сайтом за замовчуванням.


Зрозумів. Моя повістка - "як міг користувач змінити значення $ _SERVER ['HTTP_HOST']?" Чи можливо це навіть?
Джефф

5
Користувач може змінити це, оскільки це лише вміст заголовка хосту з вхідного запиту. Основний сервер (або VirtualHost, встановлений за замовчуванням : 80) відповість на всі невідомі хости, таким чином вміст тегу Host на цьому веб-сайті може бути встановлено на що завгодно.
Пауерлорд

4
Зауважте, що віртуальні хости на основі IP-адреси ВИНАГИ реагуватимуть на конкретний IP-адресу, тому ви ні за яких обставин не можете довіряти значенню хоста HTTP.
Пауерлорд

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у файл root .htaccess та додайте код THS у Common.php

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

Я включаю цей файл Common.php на кожну сторінку php. Ця сторінка робить все необхідне для кожного запиту, наприклад session_start(), змінює файли cookie сесії та відхиляє, якщо метод публікації надходить з іншого домену.


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

1

XSSзавжди буде там , навіть якщо ви використовуєте $_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']OR$_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]; }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.