PHP $ _SERVER ['HTTP_HOST'] kontra $ _SERVER ['SERVER_NAME'], czy poprawnie rozumiem strony podręcznika?

167

Dużo szukałem, a także przeczytałem dokumentację PHP $ _SERVER . Czy mam prawo do tego, których użyć w moich skryptach PHP do prostych definicji łączy używanych w mojej witrynie?

$_SERVER['SERVER_NAME'] opiera się na pliku konfiguracyjnym serwera WWW (w moim przypadku Apache2) i różni się w zależności od kilku dyrektyw: (1) VirtualHost, (2) ServerName, (3) UseCanonicalName itp.

$_SERVER['HTTP_HOST'] opiera się na żądaniu klienta.

Dlatego wydaje mi się, że właściwy byłby ten, którego należy użyć, aby moje skrypty były jak najbardziej kompatybilne $_SERVER['HTTP_HOST']. Czy to założenie jest słuszne?

Uwagi uzupełniające:

Wydaje mi się, że po przeczytaniu tego artykułu dostałem trochę paranoi i zauważyłem, że niektórzy ludzie powiedzieli „nie ufaliby żadnemu z $_SERVERvars”:

Najwyraźniej dyskusja dotyczy głównie $_SERVER['PHP_SELF']tego, dlaczego nie należy go używać w atrybucie działania formularza bez odpowiedniej ucieczki, aby zapobiec atakom XSS.

Mój wniosek dotyczący mojego pierwotnego pytania powyżej jest taki, że używanie $_SERVER['HTTP_HOST']wszystkich linków w witrynie jest „bezpieczne” bez martwienia się o ataki XSS, nawet jeśli są używane w formularzach.

Proszę popraw mnie jeżeli się mylę.

Jeff
źródło

Odpowiedzi:

149

To chyba pierwsza myśl każdego. Ale to trochę trudniejsze. Zobacz artykuł Chrisa Shifletta SERVER_NAMEVersusHTTP_HOST .

Wygląda na to, że nie ma srebrnej kuli. Tylko wtedy, gdy zmusisz Apache do używania nazwy kanonicznej , zawsze otrzymasz właściwą nazwę serwera SERVER_NAME.

Więc albo idziesz z tym albo sprawdzasz nazwę hosta na białej liście:

$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;
}
Gumbo
źródło
4
Lol, przeczytałem ten artykuł i tak naprawdę nie wydawał się odpowiedzieć na moje pytanie. Którego używają programiści? Jeśli którykolwiek.
Jeff,
2
Interesujące, nigdy nie wiedziałem, że SERVER_NAME używa domyślnie wartości podanych przez użytkownika w Apache.
Powerlord
1
@Jeff, W przypadku serwerów, które obsługują więcej niż jedną sub / domenę, masz tylko dwie możliwości $_SERVER['SERVER_NAME']i $_SERVER['HTTP_HOST'](oprócz implementacji innego niestandardowego uzgadniania na podstawie żądania użytkownika). Profesjonalni deweloperzy nie ufają rzeczom, których do końca nie rozumieją. Więc albo mają perfekcyjnie poprawną konfigurację SAPI (w takim przypadku opcja, której używają , da poprawny wynik), albo zrobią białą listę tak, że nie ma znaczenia, jakie wartości dostarcza SAPI.
Pacerier
@Gumbo, Musisz zastosować łatkę "portu" z powodu poważnych problemów z niektórymi interfejsami SAPI. Ponadto array_key_existsjest bardziej skalowalny w porównaniu z in_arraywydajnością O (n).
Pacerier
2
@Pacerier array_key_exists i in_array robią różne rzeczy, wcześniej sprawdzają klucze, drugie wartości, więc nie możesz ich po prostu zamienić. Ponadto, jeśli masz tablicę dwóch wartości, nie powinieneś się martwić o wydajność O (n) ...
eis
74

Tylko dodatkowa uwaga - jeśli serwer działa na porcie innym niż 80 (co może być powszechne na maszynie deweloperskiej / intranetowej), to HTTP_HOST zawiera port, a SERVER_NAMEnie.

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

(Tak przynajmniej zauważyłem w virtualhostach opartych na portach Apache)

Jak Mike zauważył poniżej, HTTP_HOSTczy nie zawierają :443gdy działa na HTTPS (chyba że działa na niestandardowym porcie, które nie zostały przetestowane).

Simon East
źródło
4
Uwaga: portu nie ma również w HTTP_HOST dla 443 (domyślny port SSL).
Mike,
Innymi słowy, wartość parametru HTTP_HOSTnie jest dokładnie Host:parametrem podanym przez użytkownika. Opiera się tylko na tym.
Pacerier
1
@Pacerier Nie, jest odwrotnie: HTTP_HOST jest dokładnie tym polem Host:, które zostało dostarczone z żądaniem HTTP. Port jest jego częścią i przeglądarki nie wspominają o nim, gdy jest to port domyślny (80 dla HTTP; 443 dla HTTPS)
xhienne
29

Użyj albo. Oba są jednakowo (nie) bezpieczne, ponieważ w wielu przypadkach SERVER_NAME i tak jest po prostu wstawiane z HTTP_HOST. Zwykle wybieram HTTP_HOST, więc użytkownik pozostaje przy dokładnej nazwie hosta, na którym zaczął. Na przykład, jeśli mam tę samą witrynę w domenach .com i .org, nie chcę wysyłać nikogo z .org na .com, szczególnie jeśli może mieć tokeny logowania na .org, które straciłby, gdyby został wysłany do inna domena.

Tak czy inaczej, po prostu musisz mieć pewność, że Twoja aplikacja internetowa będzie reagować tylko na znane, dobre domeny. Można to zrobić (a) za pomocą sprawdzenia po stronie aplikacji, takiego jak Gumbo, lub (b) za pomocą wirtualnego hosta w żądanych nazwach domen, który nie odpowiada na żądania, które zawierają nieznany nagłówek hosta.

Powodem tego jest to, że jeśli zezwolisz na dostęp do swojej witryny pod jakąkolwiek starą nazwą, narażasz się na ataki polegające na ponownym wiązaniu DNS (gdy nazwa hosta innej witryny wskazuje na Twój adres IP, użytkownik uzyskuje dostęp do Twojej witryny za pomocą nazwy hosta atakującego, a następnie nazwy hosta jest przenoszony na adres IP atakującego, zabierając ze sobą pliki cookie / uwierzytelnianie) i przechwytujący wyszukiwarkę (gdy atakujący wskazuje własną nazwę hosta w Twojej witrynie i próbuje sprawić, by wyszukiwarki postrzegały ją jako „najlepszą” podstawową nazwę hosta).

Najwyraźniej dyskusja dotyczy głównie $ _SERVER ['PHP_SELF'] i tego, dlaczego nie powinieneś używać go w atrybucie działania formularza bez odpowiedniej ucieczki, aby zapobiec atakom XSS.

Pfft. Cóż, nie powinieneś używać niczego w żadnym atrybucie bez ucieczki z htmlspecialchars($string, ENT_QUOTES), więc nie ma tam nic specjalnego w zmiennych serwerowych.

bobince
źródło
Pozostanie przy rozwiązaniu (a), (b) nie jest naprawdę bezpieczne, używanie bezwzględnego identyfikatora URI w żądaniach HTTP umożliwia obejście zabezpieczeń hostów wirtualnych opartych na nazwie. Tak więc prawdziwa zasada nigdy nie polega na zaufaniu do SERVER_NAME lub HTTP_HOST.
regilero
@bobince, Jak działa porwanie wspomnianej wyszukiwarki? Wyszukiwarki mapują słowa na adresy URL domen , nie zajmują się adresami IP. Dlaczego więc mówisz, że „atakujący może sprawić, że wyszukiwarki będą postrzegane attacker.comjako najlepsze podstawowe źródło adresu IP Twojego serwera”? Wydaje się, że to nic nie znaczy dla wyszukiwarek. Co to w ogóle da?
Pacerier
2
Google z pewnością miał (i prawdopodobnie nadal ma w jakiejś formie) koncepcję witryn podupadających, więc jeśli Twoja witryna jest dostępna jako http://example.com/, http://www.example.com/ihttp://93.184.216.34/ byłoby połączyć je w jednym miejscu, wybrać najbardziej popularne adresy, a jedynie powrót linki do wersja. Gdybyś mógł wskazać evil-example.comten sam adres i sprawić, by Google na krótko uznało go za bardziej popularny, możesz ukraść sok witryny. Nie wiem, jak praktyczne jest to dzisiaj, ale widziałem w przeszłości rosyjskie osoby atakujące farmy linków.
bobince
24

To jest rozwlekłe tłumaczenie tego, czego używa Symfony do uzyskania nazwy hosta ( zobacz drugi przykład dla bardziej dosłownego tłumaczenia ):

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

Przestarzały:

Oto moje tłumaczenie na gołe PHP metody używanej we frameworku Symfony, która próbuje uzyskać nazwę hosta z każdego możliwego sposobu, w kolejności najlepszych praktyk:

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);
}
antytoksyczny
źródło
1
@StefanNch Określ „w ten sposób”.
showdev
1
@showdev Naprawdę uważam, że „trudno” czytać warunek, taki jak if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])lub x = a == 1 ? True : False. Kiedy pierwszy raz to zobaczyłem, mój mózg szukał instancji $ hosta i odpowiedzi „dlaczego jest tylko jeden” = „znak?”. Zaczynam nie lubić słabych języków programowania. Wszystko jest inaczej napisane. Nie oszczędzasz czasu i nie jesteś wyjątkowy. Nie piszę kodu w ten sposób, ponieważ po jakimś czasie to ja muszę go debugować. Wygląda naprawdę niechlujnie jak na zmęczony mózg! Wiem, że mój angielski jest zagmatwany, ale przynajmniej próbuję.
StefanNch
1
Chłopaki, po prostu przeportowałem kod z Symfony. Tak to wziąłem. Mimo wszystko jest to ważne - działa i wydaje się całkiem dokładny. Ja też uważam, że to nie jest wystarczająco czytelne, ale nie miałem czasu, aby to całkowicie przepisać.
antytoksyczny
2
Dla mnie wygląda dobrze. Są to operatory trójskładnikowe i faktycznie mogą zaoszczędzić czas (i bajty) bez zmniejszania czytelności, jeśli są odpowiednio używane.
showdev
1
@antitoxic, -1 Koderzy Symfony (podobnie jak wielu innych) nie wiedzą dokładnie, co robią w tym przypadku. To nie daje nazwy hosta (patrz odpowiedź Simona). To tylko daje najlepsze przypuszczenie, które wiele razy będzie błędne.
Pacerier
11

Czy używanie $_SERVER['HTTP_HOST']wszystkich łączy w witrynie jest „bezpieczne” bez martwienia się o ataki XSS, nawet jeśli są używane w formularzach?

Tak, jest bezpieczny w użyciu $_SERVER['HTTP_HOST'](a nawet $_GETi $_POST), o ile zweryfikujesz je przed ich zaakceptowaniem. Oto, co robię dla bezpiecznych serwerów produkcyjnych:

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
$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!';
// ...

Zaletą $_SERVER['HTTP_HOST']jest to, że jego zachowanie jest lepiej zdefiniowane niż $_SERVER['SERVER_NAME']. Kontrast ➫➫ :

Zawartość hosta: nagłówek z bieżącego żądania, jeśli taki istnieje.

z:

Nazwa hosta serwera, pod którym wykonywany jest bieżący skrypt.

Używanie lepiej zdefiniowanego interfejsu, takiego jak, $_SERVER['HTTP_HOST']oznacza, że ​​więcej SAPI będzie go implementować przy użyciu niezawodnego, dobrze zdefiniowanego zachowania. (W przeciwieństwie do innych .) Jednak nadal jest całkowicie zależny od SAPI ➫➫ :

Nie ma gwarancji, że każdy serwer WWW dostarczy którekolwiek z tych [ $_SERVERwpisów]; serwery mogą pomijać niektóre lub udostępniać inne niewymienione tutaj.

Aby zrozumieć, jak prawidłowo pobrać nazwę hosta, przede wszystkim musisz zrozumieć, że serwer, który zawiera tylko kod, nie ma możliwości poznania (warunku wstępnego weryfikacji) własnej nazwy w sieci. Musi łączyć się z komponentem, który nadaje mu własną nazwę. Można to zrobić za pomocą:

  • lokalny plik konfiguracyjny

  • lokalna baza danych

  • zakodowany na stałe kod źródłowy

  • żądanie zewnętrzne ( curl )

  • Host:żądanie klienta / napastnika

  • itp

Zwykle odbywa się to za pośrednictwem lokalnego pliku konfiguracyjnego (SAPI). Zwróć uwagę, że skonfigurowałeś go poprawnie, np. W Apache ➫➫ :

Aby dynamiczny host wirtualny wyglądał jak normalny, trzeba „sfałszować” kilka rzeczy.

Najważniejsza jest nazwa serwera, z której Apache korzysta do generowania odwołujących się adresów URL itp. Jest konfigurowana za pomocą polecenia ServerName dyrektywy i jest dostępna dla CGI poprzez SERVER_NAMEzmienną środowiskową.

Rzeczywista wartość używana w czasie wykonywania jest kontrolowana przez ustawienie UseCanonicalName.

Wraz UseCanonicalName Off z nazwą serwera pochodzi z treści Host:nagłówka w żądaniu. Dzięki UseCanonicalName DNS temu pochodzi z odwrotnego wyszukiwania DNS adresu IP wirtualnego hosta. Pierwsze ustawienie jest używane do dynamicznego hostingu wirtualnego opartego na nazwach, a drugie do hostingu opartego na ** adresie IP.

Jeśli Apache nie może wypracować nazwę serwera, ponieważ nie ma Host:nagłówka lub DNS Lookup nie wtedy wartość skonfigurowaną ze ServerNamejest używany zamiast.

Pacerier
źródło
8

Główna różnica między nimi polega na tym, że $_SERVER['SERVER_NAME']jest to zmienna kontrolowana przez serwer, a $_SERVER['HTTP_HOST']wartość kontrolowana przez użytkownika.

Praktyczna zasada brzmi: nigdy nie ufaj wartościom użytkownika, więc $_SERVER['SERVER_NAME']jest to lepszy wybór.

Jak wskazał Gumbo, Apache skonstruuje SERVER_NAME z wartości podanych przez użytkownika, jeśli tego nie ustawisz UseCanonicalName On.

Edycja: Powiedziawszy to wszystko, jeśli witryna używa hosta wirtualnego opartego na nazwie, nagłówek HTTP Host jest jedynym sposobem na dotarcie do witryn, które nie są witryną domyślną.

Władca
źródło
Zrozumiany. Moje zawieszenie brzmi: „w jaki sposób użytkownik może zmienić wartość $ _SERVER ['HTTP_HOST']?” Czy to w ogóle możliwe?
Jeff,
5
Użytkownik może to zmienić, ponieważ jest to tylko zawartość nagłówka Host z przychodzącego żądania. Główny serwer (lub VirtualHost przypisany domyślnie : 80) odpowie na wszystkie nieznane hosty, więc zawartość tagu Host w tej witrynie może być dowolnie ustawiona.
Powerlord
4
Zwróć uwagę, że hosty wirtualne oparte na adresach IP ZAWSZE będą odpowiadać na swoje konkretne adresy IP, więc w żadnych okolicznościach nie możesz ufać wartości hosta HTTP na nich.
Powerlord
1
@Jeff, to tak, jakby zapytać „Czy można zadzwonić pod numer telefonu pizzerii i poprosić o rozmowę z personelem KFC?” Oczywiście, że możesz poprosić o wszystko, co chcesz. @Powerlord, To nie ma nic wspólnego z wirtualnymi hostami opartymi na IP. Twój serwer, niezależnie od wirtualnego hosta opartego na IP, czy nie, w żadnych okolicznościach nie może ufać wartości HTTP, Host:chyba że jużzweryfikowałeś , ręcznie lub przez konfigurację SAPI.
Pacerier
3

Nie jestem pewien i nie ufam, $_SERVER['HTTP_HOST']ponieważ zależy to od nagłówka klienta. Z drugiej strony, jeśli domena żądana przez klienta nie jest moją domeną, nie dostaną się do mojej witryny, ponieważ DNS i protokół TCP / IP kierują ją do właściwego miejsca docelowego. Jednak nie wiem, czy można przejąć DNS, sieć, a nawet serwer Apache. Dla pewności definiuję nazwę hosta w środowisku i porównuję z$_SERVER['HTTP_HOST'] .

Dodaj SetEnv MyHost domain.complik .htaccess na root i dodaj ten kod do Common.php

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

Dołączam ten plik Common.php do każdej strony php. Ta strona robi wszystko, co jest wymagane dla każdego żądania, na przykład session_start()modyfikuje plik cookie sesji i odrzuca, jeśli metoda wysyłania pochodzi z innej domeny.

CallMeLaNN
źródło
1
Oczywiście można ominąć DNS. Atakujący może po prostu wystawić fałszywą Host:wartość bezpośrednio na adres IP twojego serwera.
Pacerier
1

XSSzawsze będzie tam nawet jeśli używasz $_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']OR$_SERVER['PHP_SELF']

Jaydeep Dave
źródło
1

Najpierw chciałbym podziękować za wszystkie dobre odpowiedzi i wyjaśnienia. To jest metoda, którą stworzyłem na podstawie wszystkich twoich odpowiedzi, aby uzyskać podstawowy adres URL. Używam go tylko w bardzo rzadkich sytuacjach. Dlatego NIE kładzie się dużego nacisku na kwestie bezpieczeństwa, takie jak ataki XSS. Może ktoś tego potrzebuje.

// 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]; }
}
Mikrofon
źródło