PHP $ _SERVER ['HTTP_HOST'] vs. $ _SERVER ['SERVER_NAME'], ¿entiendo las páginas man correctamente?

167

Hice mucha búsqueda y también leí los documentos PHP $ _SERVER . ¿Tengo este derecho con respecto a cuál usar para mis scripts PHP para definiciones de enlaces simples usadas en mi sitio?

$_SERVER['SERVER_NAME'] se basa en el archivo de configuración de su servidor web (Apache2 en mi caso) y varía según algunas directivas: (1) VirtualHost, (2) ServerName, (3) UseCanonicalName, etc.

$_SERVER['HTTP_HOST'] se basa en la solicitud del cliente.

Por lo tanto, me parece que sería apropiado utilizarlo para que mis scripts sean lo más compatibles posible $_SERVER['HTTP_HOST']. ¿Es correcta esta suposición?

Comentarios de seguimiento:

Creo que me puse un poco paranoico después de leer este artículo y notar que algunas personas dijeron "no confiarían en ninguno de los $_SERVERvars":

Aparentemente, la discusión es principalmente sobre $_SERVER['PHP_SELF'] y por qué no debe usarlo en el atributo de acción de formulario sin escapar adecuadamente para evitar ataques XSS.

Mi conclusión sobre mi pregunta original anterior es que es "seguro" usar $_SERVER['HTTP_HOST'] para todos los enlaces en un sitio sin tener que preocuparse por los ataques XSS, incluso cuando se usan en formularios.

Por favor corrígeme si estoy equivocado.

Jeff
fuente

Respuestas:

149

Ese es probablemente el primer pensamiento de todos. Pero es un poco más difícil. Ver el artículo de Chris Shiflett SERVER_NAMEVersusHTTP_HOST .

Parece que no hay bala de plata. Solo cuando obligue a Apache a usar el nombre canónico , siempre obtendrá el nombre correcto del servidor SERVER_NAME.

Entonces, o va con eso o verifica el nombre del host con una lista blanca:

$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
fuente
44
Lol, leí ese artículo y realmente no parecía responder a mi pregunta. ¿Cuál usan los desarrolladores profesionales? Si alguno.
Jeff
2
Por interesante que sea, nunca supe que SERVER_NAME usó los valores proporcionados por el usuario de manera predeterminada en Apache.
Powerlord
1
@Jeff, para los servidores que alojan más de un sub / dominio, solo tiene dos opciones $_SERVER['SERVER_NAME']y $_SERVER['HTTP_HOST'](aparte de implementar algún otro protocolo de enlace personalizado basado en la solicitud del usuario). Los desarrolladores profesionales no confían en las cosas que no entienden completamente. Por lo tanto, tienen su configuración de SAPI perfectamente correcta (en cuyo caso la opción que usan les dará el resultado correcto), o harán una lista blanca de tal manera que no importa qué valores proporcione el SAPI.
Pacerier
@Gumbo, debe aplicar el parche "port" debido a problemas serios con ciertos SAPI. Además, array_key_existses más escalable en comparación con el in_arrayque tiene un rendimiento O (n).
Pacerier
2
@Pacerier array_key_exists e in_array hacen cosas diferentes, verificaciones anteriores de claves, valores posteriores, por lo que no puede intercambiarlos. Además, si usted tiene un conjunto de dos valores, no debe realmente estar preocupado por O (n) el rendimiento ...
EIS
74

Solo una nota adicional: si el servidor se ejecuta en un puerto que no sea 80 (como podría ser común en una máquina de desarrollo / intranet), entonces HTTP_HOSTcontiene el puerto, mientras SERVER_NAMEque no lo hace.

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

(Al menos eso es lo que he notado en los hosts virtuales basados ​​en el puerto de Apache)

Como Mike ha señalado a continuación, HTTP_HOSThace no contener :443cuando se ejecuta en HTTPS (a no ser que se está ejecutando en un puerto no estándar, que no he probado).

Simon East
fuente
44
Nota: El puerto tampoco está presente en HTTP_HOST para 443 (puerto SSL predeterminado).
Mike
En otras palabras, el valor de HTTP_HOSTno es exactamente el Host:parámetro que proporcionó el usuario. Simplemente se basa en eso.
Pacerier
1
@Pacerier No, esto es lo contrario: HTTP_HOST es exactamente el campo Host: que se suministró con la solicitud HTTP. El puerto es parte de él y los navegadores no lo mencionan cuando es el predeterminado (80 para HTTP; 443 para HTTPS)
xhienne
29

Utilizar cualquiera. Ambas son igualmente (in) seguras, ya que en muchos casos SERVER_NAME se completa de HTTP_HOST de todos modos. Normalmente elijo HTTP_HOST, para que el usuario permanezca en el nombre de host exacto en el que comenzó. Por ejemplo, si tengo el mismo sitio en un dominio .com y .org, no quiero enviar a alguien de .org a .com, particularmente si podrían tener tokens de inicio de sesión en .org que perderían si se los enviara a El otro dominio.

De cualquier manera, solo necesita asegurarse de que su aplicación web solo responderá por dominios conocidos. Esto se puede hacer (a) con una verificación del lado de la aplicación como Gumbo's, o (b) utilizando un host virtual en el (los) nombre (s) de dominio que desea que no responde a las solicitudes que dan un encabezado de Host desconocido.

La razón de esto es que si permite que se acceda a su sitio con cualquier nombre antiguo, se expone a ataques de reenlace de DNS (donde el nombre de host de otro sitio apunta a su IP, un usuario accede a su sitio con el nombre de host del atacante, luego el nombre de host se mueve a la IP del atacante, tomando sus cookies / autenticación con él) y el secuestro del motor de búsqueda (donde un atacante señala su propio nombre de host en su sitio e intenta hacer que los motores de búsqueda lo vean como el "mejor" nombre de host principal).

Aparentemente, la discusión es principalmente sobre $ _SERVER ['PHP_SELF'] y por qué no debe usarlo en el atributo de acción de formulario sin escapar adecuadamente para evitar ataques XSS.

No. Bueno, no debe usar nada en ningún atributo sin escapar htmlspecialchars($string, ENT_QUOTES), por lo que no hay nada especial sobre las variables del servidor allí.

bobince
fuente
Mantenerse con la solución (a), (b) no es realmente seguro, el uso de URI absoluto en las solicitudes HTTP permite eludir la seguridad de hosts virtuales basados ​​en nombres. Entonces, la regla real nunca es confiar en SERVER_NAME o HTTP_HOST.
regilero
@bobince, ¿Cómo funciona el secuestro de motores de búsqueda mencionado? Los motores de búsqueda asignan palabras a las URL de dominio , no tratan con IP. Entonces, ¿por qué dice que "un atacante puede hacer que los motores de búsqueda vean attacker.comcomo la mejor fuente primaria para la IP de su servidor"? Eso no parece significar nada para los motores de búsqueda, ¿qué va a hacer eso?
Pacerier
2
Google ciertamente tenía (y probablemente todavía tiene de alguna forma) el concepto de sitios engañados, de modo que si su sitio es accesible como http://example.com/, http://www.example.com/y http://93.184.216.34/los combinaría en un solo sitio, elegiría las direcciones más populares y solo devolvería enlaces a ese versión. Si pudiera apuntar evil-example.coma la misma dirección y hacer que Google vea brevemente que, como la dirección más popular, podría robar el jugo del sitio. No sé cuán práctico es esto hoy, pero he visto a atacantes rusos de granjas de enlaces intentar hacerlo en el pasado.
bobince
24

Esta es una traducción detallada de lo que Symfony usa para obtener el nombre de host ( vea el segundo ejemplo para una traducción más literal ):

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

Anticuado:

Esta es mi traducción al PHP desnudo de un método utilizado en el marco de Symfony que intenta obtener el nombre de host de todas las formas posibles en orden de mejores prácticas:

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);
}
antitóxico
fuente
1
@StefanNch Defina "de esta manera".
showdev
1
@showdev Realmente encuentro "difícil" leer la declaración de condición como if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])o x = a == 1 ? True : False. La primera vez que lo vi, mi cerebro estaba buscando una instanciación de $ host y una respuesta para "¿por qué solo hay un" = "signo?". Estoy empezando a no gustarme los lenguajes de programación de mecanografía débil. Todo está escrito de manera diferente. No ahorras tiempo y no eres especial. No escribo código de esta manera, porque después de que pase el tiempo, soy yo quien debe depurarlo. ¡Parece realmente desordenado para un cerebro cansado! Sé que mi inglés es inglés, pero al menos lo intento.
StefanNch
1
chicos, simplemente porté el código de Symfony. Así es como lo tomé. Por lo que importa, funciona y parece bastante completo. Yo, también, creo que esto no es lo suficientemente legible, pero no he tenido tiempo de reescribirlo por completo.
antitóxico
2
Luce bien para mi. Esos son operadores ternarios y en realidad pueden ahorrar tiempo (y bytes) sin disminuir la legibilidad, cuando se usan adecuadamente.
showdev
1
@antitoxic, -1 Los codificadores de Symfony (como muchos otros) no saben exactamente qué están haciendo en este caso. Esto no le da el nombre de host (vea la respuesta de Simon). Esto simplemente le da una mejor suposición que se equivocará muchas veces.
Pacerier
11

¿Es "seguro" usarlo $_SERVER['HTTP_HOST']para todos los enlaces en un sitio sin tener que preocuparse por los ataques XSS, incluso cuando se usan en formularios?

Sí, es seguro de usar $_SERVER['HTTP_HOST'], (e incluso $_GETy $_POST) siempre que los verifique antes de aceptarlos. Esto es lo que hago para servidores de producción seguros:

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

La ventaja de $_SERVER['HTTP_HOST']es que su comportamiento está más bien definido que $_SERVER['SERVER_NAME']. Contraste ➫➫ :

Contenido del host: encabezado de la solicitud actual, si hay una.

con:

El nombre del host del servidor en el que se ejecuta el script actual.

El uso de una interfaz mejor definida $_SERVER['HTTP_HOST']significa que más SAPI lo implementarán utilizando un comportamiento confiable y bien definido. (A diferencia del otro .) Sin embargo, todavía depende totalmente de SAPI ➫➫ :

No hay garantía de que cada servidor web proporcione ninguna de estas [ $_SERVERentradas]; los servidores pueden omitir algunos o proporcionar otros que no figuran aquí.

Para comprender cómo recuperar correctamente el nombre de host, primero debe comprender que un servidor que contiene solo código no tiene medios para conocer (requisito previo para verificar) su propio nombre en la red. Necesita interactuar con un componente que le proporcione su propio nombre. Esto se puede hacer a través de:

  • archivo de configuración local

  • base de datos local

  • código fuente codificado

  • solicitud externa ( curl )

  • Host:solicitud del cliente / atacante

  • etc.

Por lo general, se realiza a través del archivo de configuración local (SAPI). Tenga en cuenta que lo ha configurado correctamente, por ejemplo, en Apache ➫➫ :

Hay que 'falsificar' un par de cosas para que el host virtual dinámico se vea normal.

El más importante es el nombre del servidor que utiliza Apache para generar URL autorreferenciales, etc. Está configurado con la ServerNamedirectiva y está disponible para CGI a través deSERVER_NAME variable de entorno.

El valor real utilizado en tiempo de ejecución está controlado por la configuración UseCanonicalName.

Con UseCanonicalName Off el nombre del servidor proviene del contenido del Host:encabezado en la solicitud. Con UseCanonicalName DNS esto proviene de una búsqueda inversa de DNS de la dirección IP del host virtual. La configuración anterior se usa para el alojamiento virtual dinámico basado en nombres, y la segunda se usa para el alojamiento ** basado en IP.

Si Apache no puede calcular el nombre del servidor porque no hay Host:encabezado o la búsqueda de DNS falla, entoncesServerName se utiliza el valor configurado con .

Pacerier
fuente
8

La principal diferencia entre los dos es que $_SERVER['SERVER_NAME']es una variable controlada por el servidor, mientras que $_SERVER['HTTP_HOST']es un valor controlado por el usuario.

La regla general es no confiar nunca en los valores del usuario, por lo que $_SERVER['SERVER_NAME']es la mejor opción.

Como señaló Gumbo, Apache construirá SERVER_NAME a partir de valores proporcionados por el usuario si no establece UseCanonicalName On.

Editar: Habiendo dicho todo eso, si el sitio usa un host virtual basado en nombre, el encabezado HTTP Host es la única forma de llegar a sitios que no son el sitio predeterminado.

Powerlord
fuente
Entendido. Mi bloqueo es "¿cómo podría un usuario alterar el valor de $ _SERVER ['HTTP_HOST']?" ¿Es posible?
Jeff
55
Un usuario puede alterar eso porque es solo el contenido del encabezado Host de la solicitud entrante. El servidor principal (o VirtualHost vinculado al predeterminado : 80) responderá a todos los hosts desconocidos, por lo tanto, el contenido de la etiqueta Host en ese sitio podría establecerse en cualquier cosa.
Powerlord
44
Tenga en cuenta que los hosts virtuales basados ​​en IP SIEMPRE responderán en su IP específica, por lo que bajo ninguna circunstancia puede confiar en el valor de Host HTTP en ellos.
Powerlord
1
@Jeff, es como preguntar "¿Es posible llamar al número de teléfono de Pizza Hut y solicitar hablar con el personal de KFC?" Claro que puedes solicitar lo que quieras. @Powerlord, esto no tiene nada que ver con los hosts virtuales basados ​​en IP. Su servidor, independientemente del host virtual basado en IP o no, no puede en ningún caso confiar en el Host:valor de HTTP a menos que ya lo haya verificado , ya sea manualmente o mediante la configuración de su SAPI.
Pacerier
3

No estoy seguro y realmente no confío $_SERVER['HTTP_HOST']porque depende del encabezado del cliente. De otra manera, si un dominio solicitado por el cliente no es mío, no ingresarán a mi sitio porque los protocolos DNS y TCP / IP lo dirigen al destino correcto. Sin embargo, no sé si es posible secuestrar el DNS, la red o incluso el servidor Apache. Para estar seguro, defino el nombre de host en el entorno y lo comparo con $_SERVER['HTTP_HOST'].

Agregue el SetEnv MyHost domain.comarchivo .htaccess en la raíz y agregue este código en Common.php

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

Incluyo este archivo Common.php en cada página php. Esta página hace todo lo necesario para cada solicitud session_start(), como modificar la cookie de sesión y rechazar si el método de publicación proviene de un dominio diferente.

CallMeLaNN
fuente
1
Por supuesto, es posible evitar el DNS. Un atacante puede simplemente emitir un Host:valor fradulento directamente a la IP de su servidor.
Pacerier
1

XSSsiempre estará allí, incluso si usa $_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']O$_SERVER['PHP_SELF']

Jaydeep Dave
fuente
1

Primero quiero agradecerles por todas las buenas respuestas y explicaciones. Este es el método que creé basado en toda su respuesta para obtener la URL base. Solo lo uso en situaciones muy raras. Por lo tanto, NO hay un gran enfoque en los problemas de seguridad, como los ataques XSS. Quizás alguien lo necesite.

// 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]; }
}
Miguel
fuente