PHP $ _SERVER ['HTTP_HOST'] vs. $ _SERVER ['SERVER_NAME'], estou entendendo as páginas de manual corretamente?

167

Pesquisei bastante e também li os documentos do PHP $ _SERVER . Eu tenho esse direito sobre o que usar nos meus scripts PHP para definições simples de link usadas em todo o meu site?

$_SERVER['SERVER_NAME'] é baseado no arquivo de configuração do servidor da web (Apache2 no meu caso) e varia de acordo com algumas diretrizes: (1) VirtualHost, (2) ServerName, (3) UseCanonicalName, etc.

$_SERVER['HTTP_HOST'] é baseado na solicitação do cliente.

Portanto, parece-me que o correto a ser usado para tornar meus scripts o mais compatível possível seria $_SERVER['HTTP_HOST']. Esta suposição está correta?

Comentários de acompanhamento:

Acho que fiquei um pouco paranóico depois de ler este artigo e notar que algumas pessoas disseram "não confiariam em nenhum dos $_SERVERvars":

Aparentemente, a discussão é principalmente sobre $_SERVER['PHP_SELF']e por que você não deve usá-la no atributo action action sem escape apropriado para evitar ataques XSS.

Minha conclusão sobre minha pergunta original acima é que é "seguro" usar $_SERVER['HTTP_HOST']para todos os links em um site sem ter que se preocupar com ataques XSS, mesmo quando usado em formulários.

Por favor me corrija se eu estiver errado.

Jeff
fonte

Respostas:

149

Esse é provavelmente o primeiro pensamento de todos. Mas é um pouco mais difícil. Veja o artigo de Chris Shiflett SERVER_NAMEVersusHTTP_HOST .

Parece que não há bala de prata. Somente quando você força o Apache a usar o nome canônico, você sempre obtém o nome correto do servidor SERVER_NAME.

Então, você concorda com isso ou verifica o nome do host em uma lista branca:

$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;
}
quiabo
fonte
4
Lol, li esse artigo e realmente não pareceu responder à minha pergunta. Qual deles os profissionais devem usar? Se qualquer um.
Jeff
2
Interessante, eu nunca soube que SERVER_NAME usava os valores fornecidos pelo usuário por padrão no Apache.
Powerlord 22/09/09
1
@ Jeff, Para servidores que hospedam mais de um sub / domínio, você tem apenas duas opções $_SERVER['SERVER_NAME']e $_SERVER['HTTP_HOST'](além de implementar outro handshake personalizado com base na solicitação do usuário). Os desenvolvedores profissionais não confiam no que não entendem completamente. Então, que quer ter sua SAPI configuração perfeitamente corretamente (caso em que a opção que eles usam vai dar o resultado correto), ou eles vão fazer whitelisting de tal forma que não importa o que valoriza o fornecimento SAPI.
Pacerier
@ Gumbo, você precisa aplicar o patch "port" devido a problemas sérios em determinadas SAPIs. Além disso, array_key_existsé mais escalável em comparação com in_arrayo desempenho O (n).
Pacerier 5/03
2
O @Pacerier array_key_exists e o in_array fazem coisas diferentes, verificações anteriores de chaves e valores posteriores, então você não pode simplesmente trocá-las. Além disso, se você tiver uma matriz de dois valores, você não deve realmente se preocupar com O (n) desempenho ...
eis
74

Apenas uma observação adicional - se o servidor executar em uma porta diferente de 80 (como pode ser comum em uma máquina de desenvolvimento / intranet), ela HTTP_HOSTconterá a porta, enquanto SERVER_NAMEnão.

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

(Pelo menos foi o que notei nos virtualhosts baseados em porta do Apache)

Como Mike observou a seguir, HTTP_HOSTque não contêm :443quando executado em HTTPS (a menos que você estiver executando em uma porta não-padrão, o que eu não testei).

Simon East
fonte
4
Nota: A porta também não está presente no HTTP_HOST para 443 (porta SSL padrão).
Mike
Portanto, em outras palavras, o valor de HTTP_HOSTnão é exatamente o Host:parâmetro fornecido pelo usuário. É apenas baseado nisso.
Pacerier
1
@Pacerier Não, é o contrário: HTTP_HOST é exatamente o campo Host: que foi fornecido com a solicitação HTTP. O porto é parte dela e navegadores não mencioná-lo quando ele é o padrão (80 para HTTP; 443 para HTTPS)
xhienne
29

Use qualquer um. Ambos são igualmente (in) seguros, pois em muitos casos o SERVER_NAME é apenas preenchido a partir de HTTP_HOST de qualquer maneira. Eu normalmente uso HTTP_HOST, para que o usuário permaneça no nome exato do host em que começou. Por exemplo, se eu tiver o mesmo site em um domínio .com e .org, não quero enviar alguém de .org para .com, principalmente se eles tiverem tokens de login em .org que perderiam se enviados para o outro domínio.

De qualquer forma, você só precisa ter certeza de que o seu aplicativo da Web só responderá sempre por domínios conhecidos. Isso pode ser feito (a) com uma verificação no lado do aplicativo, como a Gumbo, ou (b) usando um host virtual no (s) nome (s) de domínio que você deseja que não responda às solicitações que fornecem um cabeçalho de host desconhecido.

A razão para isso é que, se você permitir que seu site seja acessado com qualquer nome antigo, você estará aberto a ataques de religação de DNS (onde o nome do host de outro site aponta para o seu IP, um usuário acessa o site com o nome do host do atacante e, em seguida, o nome do host é movido para o IP do invasor, levando seus cookies / autenticação com ele) e o seqüestro de mecanismo de pesquisa (onde um invasor aponta seu próprio nome de host em seu site e tenta fazer com que os mecanismos de pesquisa o vejam como o 'melhor' nome de host principal).

Aparentemente, a discussão é principalmente sobre $ _SERVER ['PHP_SELF'] e por que você não deve usá-lo no atributo action action sem escape apropriado para evitar ataques XSS.

Pfft. Bem, você não deve usar nada em nenhum atributo sem escapar htmlspecialchars($string, ENT_QUOTES), portanto não há nada de especial nas variáveis ​​do servidor.

bobince
fonte
Fique com a solução (a), (b) não é realmente seguro, o uso de URI absoluto em solicitações HTTP permite o desvio de segurança de virtualhosts baseado em nome. Portanto, a regra real nunca é confiar em SERVER_NAME ou HTTP_HOST.
regilero
@obince, Como funciona o seqüestro de mecanismo de pesquisa mencionado? Os mecanismos de pesquisa mapeiam palavras para URLs de domínio , eles não lidam com IPs. Então, por que você diz que "um invasor pode fazer com que os mecanismos de pesquisa vejam attacker.comcomo a melhor fonte primária para o IP do seu servidor"? Isso não parece significar nada para os mecanismos de pesquisa. O que isso vai fazer?
Pacerier
2
Google certamente tinha (e provavelmente ainda tem de alguma forma) o conceito de sites de duplicatas, de modo que se o site é acessível como http://example.com/, http://www.example.com/e http://93.184.216.34/seria combiná-los em um site, escolher o mais popular dos endereços, e retornar apenas links para que versão. Se você pudesse apontar evil-example.compara o mesmo endereço e fazer com que o Google visse brevemente, como o endereço mais popular, você poderia roubar o suco do site. Não sei como isso é prático hoje, mas já vi invasores russos de fazendas de links tentarem fazer isso no passado.
bobince
24

Esta é uma tradução detalhada do que o Symfony usa para obter o nome do host ( veja o segundo exemplo para uma tradução mais 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);
}

Desatualizado:

Esta é minha tradução para simplificar o PHP de um método usado no framework Symfony que tenta obter o nome do host de todas as maneiras possíveis, na ordem das melhores práticas:

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
fonte
1
@StefanNch Por favor, defina "desta maneira".
showdev 22/07
1
@showdev Eu realmente acho "difícil" ler a declaração de condição como if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])ou x = a == 1 ? True : False. A primeira vez que o vi, meu cérebro estava procurando por instanciação $ host e uma resposta para "por que é apenas um" = "sinal?". Estou começando a não gostar de linguagens de programação de digitação fracas. Tudo está escrito de forma diferente. Você não economiza tempo e não é especial. Não escrevo código dessa maneira, porque depois que o tempo passa, sou eu quem precisa depurá-lo. Parece realmente confuso para um cérebro cansado! Eu sei que meu inglês é interessante, mas pelo menos eu tento.
23414 StefanNch
1
pessoal, eu simplesmente enviei o código do Symfony. Esta é a maneira que eu tomei. Por tudo o que importa - funciona e parece bastante completo. Eu também acho que isso não é legível o suficiente, mas ainda não tive tempo de reescrevê-lo completamente.
Antitóxico
2
Parece bom para mim. Esses são operadores ternários e, na verdade, podem economizar tempo (e bytes) sem diminuir a legibilidade, quando usados ​​adequadamente.
showdev 24/07
1
@antitoxic, -1 Os codificadores Symfony (como muitos outros) não sabem exatamente o que estão fazendo neste caso. Isso não fornece o nome do host (veja a resposta de Simon). Isso apenas fornece uma melhor estimativa, que estará errada muitas vezes.
Pacerier
11

É "seguro" usar $_SERVER['HTTP_HOST']para todos os links em um site sem ter que se preocupar com ataques XSS, mesmo quando usado em formulários?

Sim, é seguro usar $_SERVER['HTTP_HOST'](e até $_GETe $_POST) desde que você os verifique antes de aceitá-los. Isto é o que faço para servidores de produção 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!';
// ...

A vantagem $_SERVER['HTTP_HOST']é que seu comportamento é mais bem definido do que $_SERVER['SERVER_NAME']. Contraste ➫➫ :

Conteúdo do host: cabeçalho da solicitação atual, se houver.

com:

O nome do host do servidor sob o qual o script atual está sendo executado.

Usar uma interface melhor definida como $_SERVER['HTTP_HOST']significa que mais SAPIs a implementarão usando um comportamento bem definido e confiável . (Diferente do outro .) No entanto, ainda é totalmente dependente do SAPI ➫➫ :

Não há garantia de que todo servidor web forneça qualquer uma dessas [ $_SERVERentradas]; servidores podem omitir alguns ou fornecer outros não listados aqui.

Para entender como recuperar corretamente o nome do host, primeiro e acima de tudo, você precisa entender que um servidor que contém apenas código não tem como saber (pré-requisito para verificar) seu próprio nome na rede. Ele precisa interagir com um componente que fornece seu próprio nome. Isso pode ser feito via:

  • arquivo de configuração local

  • banco de dados local

  • código fonte codificado

  • solicitação externa ( ondulação )

  • Host:solicitação do cliente / atacante

  • etc

Geralmente é feito através do arquivo de configuração local (SAPI). Observe que você o configurou corretamente, por exemplo, no Apache ➫➫ :

Algumas coisas precisam ser "falsificadas" para fazer com que o host virtual dinâmico pareça normal.

O mais importante é o nome do servidor usado pelo Apache para gerar URLs auto-referenciais etc. Ele é configurado com a ServerNamediretiva e está disponível para CGIs por meio doSERVER_NAME meio da variável de ambiente.

O valor real usado no tempo de execução é controlado pela configuração UseCanonicalName.

Com UseCanonicalName Off o nome do servidor, vem do conteúdo do Host:cabeçalho na solicitação. Com UseCanonicalName DNS isso, vem de uma pesquisa DNS reversa do endereço IP do host virtual. A configuração anterior é usada para hospedagem virtual dinâmica baseada em nome e a última é usada para ** hospedagem baseada em IP.

Se o Apache não puder descobrir o nome do servidor porque não há Host:cabeçalho ou a pesquisa de DNS falhar , o valor configurado com ServerNameserá usado.

Pacerier
fonte
8

A principal diferença entre os dois é que $_SERVER['SERVER_NAME']é uma variável controlada pelo servidor, enquanto $_SERVER['HTTP_HOST']é um valor controlado pelo usuário.

A regra geral é nunca confiar nos valores do usuário, assim $_SERVER['SERVER_NAME']é a melhor escolha.

Como Gumbo apontou, o Apache criará SERVER_NAME a partir dos valores fornecidos pelo usuário, se você não definir UseCanonicalName On.

Edit: Dito tudo isso, se o site estiver usando um host virtual baseado em nome, o cabeçalho HTTP Host é a única maneira de acessar sites que não são o site padrão.

Powerlord
fonte
Entendido. Meu hangup é "como um usuário pode alterar o valor de $ _SERVER ['HTTP_HOST']?" Isso é possível?
Jeff Jeff
5
Um usuário pode alterar isso porque é apenas o conteúdo do cabeçalho Host da solicitação recebida. O servidor principal (ou o VirtualHost vinculado ao padrão : 80) responderá a todos os hosts desconhecidos; portanto, o conteúdo da tag Host nesse site pode ser definido como qualquer coisa.
Powerlord 22/09/09
4
Observe que os hosts virtuais baseados em IP responderão SEMPRE em seus IPs específicos, portanto, em nenhuma circunstância , você poderá confiar no valor do host HTTP neles.
Powerlord 22/09/09
1
@ Jeff, é como perguntar "É possível ligar para o número de telefone da pizzaria e pedir para falar com a equipe da KFC?" Claro que você pode solicitar o que quiser. @ Powerlord, isso não tem nada a ver com hosts virtuais baseados em IP. Seu servidor, independentemente do host virtual baseado em IP ou não, não pode, sob nenhuma circunstância, confiar no Host:valor do HTTP, a menos que você já o tenha verificado , manualmente ou através da instalação do SAPI.
Pacerier
3

Não tenho certeza e não confio realmente $_SERVER['HTTP_HOST']porque depende do cabeçalho do cliente. De outra maneira, se um domínio solicitado pelo cliente não for o meu, eles não entrarão no meu site porque os protocolos DNS e TCP / IP apontam para o destino correto. No entanto, não sei se é possível seqüestrar o DNS, a rede ou mesmo o servidor Apache. Para ser seguro, defino o nome do host no ambiente e o comparo com$_SERVER['HTTP_HOST'] .

Adicione o SetEnv MyHost domain.comarquivo .htaccess na raiz e adicione o código ths no Common.php

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

Eu incluo esse arquivo Common.php em todas as páginas php. Esta página faz o que for necessário para cada solicitação session_start(), como modificar o cookie da sessão e rejeitar se o método post vier de um domínio diferente.

CallMeLaNN
fonte
1
Claro que é possível ignorar o DNS. Um invasor pode simplesmente emitir um Host:valor irrelevante diretamente para o IP do seu servidor.
Pacerier
1

XSSsempre estará lá, mesmo se você usar $_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']OU$_SERVER['PHP_SELF']

Jaydeep Dave
fonte
1

Primeiro, quero agradecer por todas as boas respostas e explicações. Este é o método que eu criei com base em todas as suas respostas para obter o URL base. Só o uso em situações muito raras. Portanto, NÃO existe um grande foco em questões de segurança, como ataques XSS. Talvez alguém precise.

// 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
fonte