PHP $ _SERVER ['HTTP_HOST'] contre $ _SERVER ['SERVER_NAME'], est-ce que je comprends correctement les pages de manuel?

167

J'ai fait beaucoup de recherches et j'ai également lu la documentation PHP $ _SERVER . Ai-je ce droit sur lequel utiliser pour mes scripts PHP pour les définitions de liens simples utilisées sur mon site?

$_SERVER['SERVER_NAME'] est basé sur le fichier de configuration de votre serveur Web (Apache2 dans mon cas), et varie en fonction de quelques directives: (1) VirtualHost, (2) ServerName, (3) UseCanonicalName, etc.

$_SERVER['HTTP_HOST'] est basé sur la demande du client.

Par conséquent, il me semble que le bon à utiliser pour rendre mes scripts aussi compatibles que possible serait $_SERVER['HTTP_HOST']. Cette hypothèse est-elle correcte?

Commentaires de suivi:

Je suppose que je suis devenu un peu paranoïaque après avoir lu cet article et noté que certains ont dit "qu'ils ne feraient confiance à aucun des $_SERVERvars":

Apparemment, la discussion porte principalement sur les $_SERVER['PHP_SELF']raisons pour lesquelles vous ne devriez pas l'utiliser dans l'attribut form action sans échapper correctement pour empêcher les attaques XSS.

Ma conclusion à propos de ma question initiale ci-dessus est qu'il est "sûr" de l'utiliser $_SERVER['HTTP_HOST']pour tous les liens d'un site sans avoir à se soucier des attaques XSS, même lorsqu'il est utilisé dans des formulaires.

Corrigez-moi si j'ai tort, s'il-vous plait.

Jeff
la source

Réponses:

149

C'est probablement la première pensée de tout le monde. Mais c'est un peu plus difficile. Voir l'article SERVER_NAMEVersus deHTTP_HOST Chris Shiflett .

Il semble qu'il n'y ait pas de solution miracle. Ce n'est que lorsque vous forcez Apache à utiliser le nom canonique que vous obtiendrez toujours le bon nom de serveur SERVER_NAME.

Donc, soit vous allez avec cela, soit vous vérifiez le nom d'hôte par rapport à une liste blanche:

$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;
}
Gombo
la source
4
Lol, j'ai lu cet article et il ne semblait pas vraiment répondre à ma question. Lequel les développeurs professionnels utilisent-ils? Si non plus.
Jeff
2
Intéressant, je n'ai jamais su que SERVER_NAME utilisait les valeurs fournies par l'utilisateur par défaut dans Apache.
Powerlord
1
@Jeff, Pour les serveurs qui hébergent plus d'un sous / domaine, vous n'avez que deux choix $_SERVER['SERVER_NAME']et $_SERVER['HTTP_HOST'](mis à part l'implémentation d'une autre poignée de main personnalisée en fonction de la demande de l'utilisateur). Les développeurs professionnels ne font pas confiance aux choses qu'ils ne comprennent pas complètement. Ils ont donc soit leur SAPI configuration parfaitement correctement (dans ce cas , l'option qu'ils utilisent va donner le résultat correct), ou ils vont faire une liste blanche telle qu'elle n'a pas d' importance ce que les valeurs de l'offre SAPI.
Pacerier
@Gumbo, vous devez appliquer le correctif "port" en raison de graves problèmes avec certains SAPI. Aussi, array_key_existsest plus évolutive par rapport à in_arrayce que a O (n) performance.
Pacerier
2
@Pacerier array_key_exists et in_array font des choses différentes, le premier vérifie les clés, les dernières valeurs, donc vous ne pouvez pas simplement les échanger. De plus, si vous avez un tableau de deux valeurs, vous ne devriez pas vraiment vous inquiéter des performances O (n) ...
eis
74

Juste une remarque supplémentaire - si le serveur fonctionne sur un port autre que 80 (comme cela pourrait être courant sur une machine de développement / intranet), il HTTP_HOSTcontient alors le port, alors que ce SERVER_NAMEn'est pas le cas.

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

(Du moins, c'est ce que j'ai remarqué dans les hôtes virtuels basés sur le port Apache)

Comme Mike l'a noté ci-dessous, HTTP_HOSTne contient pas:443 lors de l'exécution sur HTTPS (sauf si vous utilisez un port non standard, que je n'ai pas testé).

Simon Est
la source
4
Remarque: Le port n'est pas non plus présent dans HTTP_HOST pour 443 (port SSL par défaut).
Mike du
En d'autres termes, la valeur de HTTP_HOSTn'est pas exactement le Host:paramètre fourni par l'utilisateur. Il est simplement basé sur cela.
Pacerier
1
@Pacerier Non, c'est le contraire: HTTP_HOST est exactement le champ Host: qui a été fourni avec la requête HTTP. Le port en fait partie et les navigateurs ne le mentionnent pas lorsqu'il s'agit du port par défaut (80 pour HTTP; 443 pour HTTPS)
xhienne
29

Utilisez l'un ou l'autre. Ils sont tous les deux également (in) sécurisés, car dans de nombreux cas, SERVER_NAME est de toute façon alimenté à partir de HTTP_HOST. Je choisis normalement HTTP_HOST, afin que l'utilisateur reste sur le nom d'hôte exact sur lequel il a commencé. Par exemple, si j'ai le même site sur un domaine .com et .org, je ne veux pas envoyer quelqu'un de .org vers .com, en particulier s'il peut avoir des jetons de connexion sur .org qu'ils perdraient s'ils étaient envoyés à l'autre domaine.

Dans tous les cas, vous devez simplement vous assurer que votre application Web ne répondra que pour les domaines connus. Cela peut être fait soit (a) avec une vérification côté application comme celle de Gumbo, ou (b) en utilisant un hôte virtuel sur le (s) nom (s) de domaine que vous voulez et qui ne répond pas aux demandes qui donnent un en-tête Host inconnu.

La raison en est que si vous autorisez l'accès à votre site sous n'importe quel ancien nom, vous vous exposez aux attaques de rebinding DNS (où le nom d'hôte d'un autre site pointe vers votre IP, un utilisateur accède à votre site avec le nom d'hôte de l'attaquant, puis le nom d'hôte. est déplacé vers l'adresse IP de l'attaquant, emportant vos cookies / authentification avec lui) et le piratage des moteurs de recherche (où un attaquant pointe son propre nom d'hôte sur votre site et essaie de faire en sorte que les moteurs de recherche le voient comme le `` meilleur '' nom d'hôte principal).

Apparemment, la discussion porte principalement sur $ _SERVER ['PHP_SELF'] et pourquoi vous ne devriez pas l'utiliser dans l'attribut d'action de formulaire sans échapper correctement pour empêcher les attaques XSS.

Pfft. Eh bien , vous ne devriez pas utiliser quoi que ce soit dans tout attribut sans échapper avec htmlspecialchars($string, ENT_QUOTES), donc il y a des variables du serveur spécial à propos de rien.

bobince
la source
Rester avec la solution (a), (b) n'est pas vraiment sûr, l'utilisation de l'URI absolu dans les requêtes HTTP permet le contournement de la sécurité des hôtes virtuels basés sur le nom. Donc, la vraie règle est de ne jamais faire confiance à SERVER_NAME ou HTTP_HOST.
regilero
@bobince, Comment fonctionne le détournement de moteur de recherche mentionné? Les moteurs de recherche mappent les mots sur les URL de domaine , ils ne traitent pas les adresses IP. Alors pourquoi dites-vous que "un attaquant peut faire en sorte que les moteurs de recherche soient considérés attacker.comcomme la meilleure source principale pour l'adresse IP de votre serveur"? Cela ne semble rien signifier pour les moteurs de recherche, qu'est-ce que cela va faire?
Pacerier
2
Google avait certainement (et a probablement encore sous une forme ou une autre) le concept de sites de dupe, de sorte que si votre site est accessible en tant que http://example.com/,http://www.example.com/ et http://93.184.216.34/qu'il les combinerait en un seul site, choisissez la plus populaire des adresses et ne renvoyez que les liens vers cela. version. Si vous pouviez pointer evil-example.comvers la même adresse et faire en sorte que Google voie brièvement cela comme l'adresse la plus populaire, vous pourriez voler le jus du site. Je ne sais pas à quel point c'est pratique aujourd'hui, mais j'ai vu des attaquants de fermes de liens russes essayer de le faire dans le passé.
bobince
24

Ceci est une traduction détaillée de ce que Symfony utilise pour obtenir le nom d'hôte ( voir le deuxième exemple pour une traduction plus littérale ):

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

Dépassé:

Ceci est ma traduction en PHP nu d'une méthode utilisée dans le framework Symfony qui tente d'obtenir le nom d'hôte de toutes les manières possibles dans l'ordre des meilleures pratiques:

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);
}
antitoxique
la source
1
@StefanNch Veuillez définir "par ici".
showdev
1
@showdev Je trouve vraiment "difficile" de lire une déclaration de condition comme if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])ou x = a == 1 ? True : False. La première fois que je l'ai vu, mon cerveau cherchait une instanciation de $ host et une réponse pour "pourquoi un seul" = "signe?" Je commence à détester les langages de programmation de frappe faibles. Tout est écrit différemment. Vous ne gagnez pas de temps et vous n'êtes pas spécial. Je n'écris pas de code de cette façon, car après le temps, c'est moi qui ai besoin de le déboguer. Ça a l'air vraiment en désordre pour un cerveau fatigué! Je sais que mon anglais est engrish, mais au moins j'essaye.
StefanNch
1
les gars, j'ai simplement porté le code de Symfony. C'est comme ça que je l'ai pris. Pour tout ce qui compte, cela fonctionne et cela semble assez complet. Moi aussi, je pense que ce n'est pas assez lisible mais je n'ai pas eu le temps de le réécrire complètement.
antitoxique le
2
Ça me va. Ce sont des opérateurs ternaires et peuvent en fait gagner du temps (et des octets) sans diminuer la lisibilité, lorsqu'ils sont utilisés de manière appropriée.
showdev
1
@antitoxic, -1 Les codeurs Symfony (comme beaucoup d'autres) ne savent pas exactement ce qu'ils font dans ce cas. Cela ne vous donne pas le nom d'hôte (voir la réponse de Simon). Cela vous donne simplement une meilleure estimation qui sera erronée plusieurs fois.
Pacerier
11

Est-il "sûr" d'utiliser $_SERVER['HTTP_HOST'] pour tous les liens sur un site sans avoir à se soucier des attaques XSS, même lorsqu'ils sont utilisés dans des formulaires?

Oui, c'est sûr à utiliser $_SERVER['HTTP_HOST'], (et même $_GETet $_POST) tant que vous les vérifiez avant de les accepter. Voici ce que je fais pour les serveurs de production sécurisés:

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

L'avantage de $_SERVER['HTTP_HOST']est que son comportement est plus bien défini que $_SERVER['SERVER_NAME']. Contraste ➫➫ :

Contenu de l'hôte: en-tête de la requête en cours, s'il y en a un.

avec:

Le nom de l'hôte du serveur sous lequel le script actuel s'exécute.

L'utilisation d'une interface mieux définie comme cela $_SERVER['HTTP_HOST']signifie que plus de SAPI l'implémenteront en utilisant un comportement fiable et bien défini. (Contrairement à l'autre .) Cependant, il est encore totalement dépendant de SAPI ➫➫ :

Il n'y a aucune garantie que chaque serveur Web fournira l'un de ces [$_SERVER entrées]; les serveurs peuvent en omettre certains ou en fournir d’autres non répertoriés ici.

Pour comprendre comment récupérer correctement le nom d'hôte, vous devez d'abord et avant tout comprendre qu'un serveur qui ne contient que du code n'a aucun moyen de connaître (condition préalable à la vérification) son propre nom sur le réseau. Il doit s'interfacer avec un composant qui lui fournit son propre nom. Cela peut être fait via:

  • fichier de configuration local

  • base de données locale

  • code source codé en dur

  • requête externe ( curl )

  • Host:demande du client / attaquant

  • etc

Habituellement, cela se fait via le fichier de configuration local (SAPI). Notez que vous l'avez configuré correctement, par exemple dans Apache ➫➫ :

Un certain nombre de choses doivent être «truquées» pour que l'hôte virtuel dynamique ressemble à un hôte normal.

Le plus important est le nom du serveur qui est utilisé par Apache pour générer des URL auto-référentielles, etc. Il est configuré avec la ServerNamedirective, et il est disponible pour les CGI via la SERVER_NAMEvariable d'environnement.

La valeur réelle utilisée au moment de l'exécution est contrôlée par le paramètre UseCanonicalName.

Avec UseCanonicalName Off le nom du serveur provient du contenu de l'en- Host:tête de la demande. Avec UseCanonicalName DNS elle vient d'une recherche inversée DNS de l'adresse IP de l'hôte virtuel. Le premier paramètre est utilisé pour l'hébergement virtuel dynamique basé sur le nom, et le dernier est utilisé pour l'hébergement ** basé sur IP.

Si Apache ne peut pas travailler sur le nom du serveur parce qu'il n'y a pas d'en- Host:tête ou le DNS recherche échoue alors la valeur configurée avec ServerNameest utilisé à la place.

Pacerier
la source
8

La principale différence entre les deux est qu'il $_SERVER['SERVER_NAME']s'agit d'une variable contrôlée par le serveur, tandis que$_SERVER['HTTP_HOST'] c'est une valeur contrôlée par l'utilisateur.

La règle d'or est de ne jamais faire confiance aux valeurs de l'utilisateur, donc $_SERVER['SERVER_NAME'] le meilleur choix.

Comme Gumbo l'a souligné, Apache construira SERVER_NAME à partir des valeurs fournies par l'utilisateur si vous ne définissez pas UseCanonicalName On .

Edit: Cela dit, si le site utilise un hôte virtuel basé sur le nom, l'en-tête HTTP Host est le seul moyen d'atteindre des sites qui ne sont pas le site par défaut.

Powerlord
la source
Compris. Mon accrochage est "Comment un utilisateur pourrait-il modifier la valeur de $ _SERVER ['HTTP_HOST']?" Est-ce même possible?
Jeff le
5
Un utilisateur peut modifier cela car il ne s'agit que du contenu de l'en-tête Host de la demande entrante. Le serveur principal (ou le VirtualHost lié à la valeur par défaut : 80) répondra à tous les hôtes inconnus, ainsi le contenu de la balise Host sur ce site pourrait être défini sur n'importe quoi.
Powerlord
4
Notez que les hôtes virtuels basés sur IP répondront TOUJOURS sur leur adresse IP spécifique, vous ne pouvez donc en aucun cas faire confiance à la valeur de l'hôte HTTP sur eux.
Powerlord
1
@Jeff, c'est comme demander "Il est possible d'appeler le numéro de téléphone de Pizza Hut et de demander à parler au personnel de KFC?" Bien sûr, vous pouvez demander tout ce que vous voulez. @Powerlord, cela n'a rien à voir avec les hôtes virtuels basés sur IP. Votre serveur, quel que soit l'hôte virtuel basé sur IP ou non, ne peut en aucun cas faire confiance à la Host:valeur HTTP sauf si vous l'avez déjà vérifiée , soit manuellement, soit via la configuration de votre SAPI.
Pacerier
3

Je ne suis pas sûr et pas vraiment confiance $_SERVER['HTTP_HOST']car cela dépend de l'en-tête du client. D'une autre manière, si un domaine demandé par le client n'est pas le mien, ils n'entreront pas dans mon site car les protocoles DNS et TCP / IP le pointent vers la bonne destination. Cependant, je ne sais pas s'il est possible de détourner le DNS, le réseau ou même le serveur Apache. Pour être sûr, je définis le nom d'hôte dans l'environnement et je le compare avec $_SERVER['HTTP_HOST'].

Ajoutez le SetEnv MyHost domain.comfichier .htaccess à la racine et ajoutez le code dans Common.php

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

J'inclus ce fichier Common.php dans chaque page php. Cette page fait tout ce qui est nécessaire pour chaque demande, comme session_start()modifier le cookie de session et rejeter si la méthode de publication provient d'un domaine différent.

CallMeLaNN
la source
1
Bien sûr, il est possible de contourner le DNS. Un attaquant peut simplement émettre une Host:valeur fradulente directement sur l'IP de votre serveur.
Pacerier
1

XSSsera toujours là même si vous utilisez $_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']OU$_SERVER['PHP_SELF']

Jaydeep Dave
la source
1

Je tiens d'abord à vous remercier pour toutes les bonnes réponses et explications. C'est la méthode que j'ai créée en fonction de toutes vos réponses pour obtenir l'URL de base. Je ne l'utilise que dans de très rares situations. Il n'y a donc PAS une grande concentration sur les problèmes de sécurité, comme les attaques XSS. Peut-être que quelqu'un en a besoin.

// 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]; }
}
Mike
la source