Em resumo: Evita o SSRF em PHP fazendo com que a sua aplicação consulte apenas endereços que constem de uma lista de permissão fixa, exclusivamente por HTTPS e sem seguir redirecionamentos. O endereço IP resolvido é verificado contra intervalos privados e reservados e depois fixado para a ligação. Uma firewall para o tráfego de saída garante que os sistemas internos permanecem inacessíveis mesmo em caso de erro no código.
Quando o próprio servidor faz pedidos por conta de terceiros
Pré-visualizações de links num chat, importação de uma imagem de produto por URL, webhooks para sistemas de parceiros, um leitor de feeds no CMS: muitas aplicações PHP consultam endereços indicados por um utilizador. É uma função útil e, ao mesmo tempo, uma porta para o interior.
No Server-Side Request Forgery, abreviadamente SSRF, um atacante leva a aplicação a enviar um pedido para um destino à sua escolha. O truque: o pedido vem do próprio servidor. Assim, alcança sistemas bloqueados a partir do exterior, como bases de dados, interfaces de administração na rede interna, serviços que escutam apenas em localhost ou os serviços de metadados dos fornecedores de nuvem.
No OWASP Top 10 de 2021, o SSRF era uma categoria própria. Em projetos PHP, a falha surge com frequência onde alguém construiu depressa uma função “adicionar imagem por URL”.
SSRF em PHP: que funções consultam endereços
Não é só o cURL que consulta endereços. Deve ter em conta os seguintes pontos:
| Função ou área | Porque é relevante |
|---|---|
cURL (curl_exec()) |
via mais comum, muitos protocolos possíveis |
file_get_contents(), fopen(), copy() com URL |
usam wrappers de URL quando allow_url_fopen está ativo |
getimagesize() e bibliotecas de imagem |
podem carregar ficheiros remotos |
| processamento de XML, clientes SOAP | podem carregar recursos externos |
| bibliotecas HTTP como o Guzzle | assentam em cURL ou streams, mesmas regras |
Se a sua aplicação não precisa de abrir ficheiros remotos através de funções de ficheiro, desative allow_url_fopen no servidor. Um tema relacionado, a inclusão de ficheiros através de caminhos do pedido, é tratado no guia Evitar Local File Inclusion em PHP: LFI e RFI.
A defesa em cinco passos
A OWASP recomenda uma combinação de medidas que, individualmente, poderiam ser contornadas, mas em conjunto sustentam a proteção:
- Lista de permissão em vez de URL livre: o ideal é o utilizador escolher apenas entre destinos conhecidos, ou indicar apenas um identificador, e a aplicação construir o endereço por si.
- Permitir apenas HTTPS: excluir no cURL outros protocolos como
file,gopherouftp, também nos redirecionamentos. - Não seguir redirecionamentos: um endereço permitido pode, de outro modo, redirecionar para um destino interno.
- Verificar e fixar o endereço resolvido: resolver o nome do anfitrião, verificar o IP contra intervalos privados, de loopback e link-local e usar exatamente esse IP para a ligação. Caso contrário, uma segunda resolução DNS pode devolver outro destino.
- Limitar a rede: o servidor web só deve alcançar para o exterior os destinos de que precisa.
Um padrão seguro com cURL
O exemplo seguinte implementa os passos 1 a 4 para IPv4. Os nomes de anfitrião e o fluxo são exemplos; para IPv6 aplicam-se as mesmas verificações.
<?php
declare(strict_types=1);
function ladeVonPartner(string $url): string
{
$teile = parse_url($url);
if (!is_array($teile) || ($teile['scheme'] ?? '') !== 'https'
|| isset($teile['user']) || isset($teile['pass']) || isset($teile['port'])) {
throw new InvalidArgumentException('Adresse nicht erlaubt.');
}
$host = strtolower($teile['host'] ?? '');
$erlaubt = ['api.partner.example', 'bilder.partner.example'];
if (!in_array($host, $erlaubt, true)) {
throw new InvalidArgumentException('Ziel nicht erlaubt.');
}
$ip = gethostbyname($host);
$oeffentlich = filter_var($ip, FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
if ($oeffentlich === false) {
throw new RuntimeException('Ziel löst auf eine nicht erlaubte Adresse auf.');
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_RESOLVE => [$host . ':443:' . $ip],
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 10,
]);
$antwort = curl_exec($ch);
if ($antwort === false) {
throw new RuntimeException('Abruf fehlgeschlagen.');
}
return $antwort;
}
Alguns pormenores são importantes. O gethostbyname() devolve o nome do anfitrião em caso de erro; a verificação de IP seguinte apanha isso. As flags FILTER_FLAG_NO_PRIV_RANGE e FILTER_FLAG_NO_RES_RANGE excluem, segundo o manual do PHP, entre outros, os intervalos 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8 e 169.254.0.0/16. Desde o PHP 8.2 existe ainda FILTER_FLAG_GLOBAL_RANGE, que admite apenas endereços globalmente válidos. O CURLOPT_RESOLVE faz com que o cURL use exatamente o endereço verificado, mantendo o nome do anfitrião e a verificação do certificado.
Dois pontos adicionais limitam o que um atacante aprende com uma falha de SSRF. Não devolva ao utilizador, sem filtro, as respostas e mensagens de erro do destino consultado: só a distinção entre “ligação recusada” e “tempo esgotado” revela que serviços internos existem. E limite o tempo de execução e o tamanho da resposta, para que a função não sirva para sobrecarregar o próprio servidor. Os webhooks para endereços que os clientes registam eles próprios enviam-se melhor a partir de uma fila de espera, e não durante o pedido em curso.
Se a sua aplicação tem mesmo de consultar endereços arbitrários, por exemplo para pré-visualizações de links, externalize essa função para um serviço separado, sem acesso à rede interna. Uma lista de permissão não é então possível, e a separação de rede é ainda mais importante.
A camada de rede como rede de segurança
As verificações no código podem ter falhas, por exemplo quando duas bibliotecas decompõem um URL de formas diferentes. A OWASP aconselha a tratar tais incoerências como rejeição e a limitar adicionalmente ao nível da rede. Um servidor web precisa, para o exterior, normalmente de poucos destinos: fontes de pacotes, prestadores de pagamento e de expedição, relay de correio, determinadas interfaces.
Uma firewall para o tráfego de saída ajuda duas vezes. Trava pedidos SSRF para a rede interna e dificulta, após uma intrusão, por exemplo através de uma versão PHP sem patch, o descarregamento de mais ferramentas. Como construir as regras é descrito em Firewall para servidores web: abrir só as portas necessárias. Que ligações um servidor estabelece efetivamente é mostrado em Verificar as ligações de saída de um servidor.
Em ambientes de nuvem, faz parte a variante reforçada do serviço de metadados. A OWASP indica, para a AWS, a mudança para o IMDSv2 e a desativação do IMDSv1.
Encontrar SSRF no código existente
Um bom ponto de partida é a pesquisa dos pontos em que funções de consulta recebem valores do pedido:
grep -rnE "(curl_init|file_get_contents|fopen|getimagesize|copy)\s*\(" src/ --include="*.php" | grep -E '\$_(GET|POST|REQUEST)|\$url'
grep -rn "CURLOPT_FOLLOWLOCATION" src/ --include="*.php"
Os resultados são tarefas de verificação. Veja com especial cuidado as funções que os administradores usam: importações, definições de webhooks, verificações de atualização de plugins.
Passam facilmente despercebidos os geradores de PDF que convertem HTML com imagens ou folhas de estilo incorporadas em documentos. Se parte desse HTML vier de entradas do utilizador, por exemplo em faturas com logótipo de livre escolha, o gerador pode consultar endereços determinados pelo utilizador. Verifique aí as definições para o carregamento de recursos remotos e desative-o se não for necessário.
Como ajuda concretamente a PHP SOS?
- Encontramos todos os pontos em que a sua aplicação consulta endereços, também em plugins e bibliotecas.
- Montamos uma função de consulta central com lista de permissão, restrição de protocolos e verificação de IP.
- Definimos regras para o tráfego de saída e protegemos os serviços de metadados em ambientes de nuvem.
- Verificamos a implementação a partir do exterior e documentamos os destinos permitidos.
Uma verificação independente da sua aplicação é descrita em Auditoria de segurança e teste de intrusão.
Perguntas frequentes
Que funções PHP podem ser abusadas para SSRF?
Todas as que consultam um endereço proveniente de entradas do utilizador: cURL, file_get_contents e fopen com wrappers de URL, funções de imagem e XML que carregam ficheiros remotos, bem como bibliotecas para webhooks, feeds ou pré-visualizações de links.
Basta bloquear endereços IP internos?
Por si só, não. A OWASP considera as listas de bloqueio fáceis de contornar e recomenda listas de permissão. Além disso, a aplicação tem de usar exatamente o endereço que verificou e não pode seguir redirecionamentos.
Porque é que o SSRF é particularmente crítico na nuvem?
Os fornecedores de nuvem disponibilizam às instâncias serviços internos de metadados, através dos quais é por vezes possível obter credenciais. Se uma aplicação consegue enviar pedidos para lá, um atacante pode alcançar esses dados através dela. Os fornecedores oferecem variantes reforçadas desses serviços, que devem estar ativadas.
O que traz uma firewall para o tráfego de saída?
Limita os destinos que o servidor consegue sequer alcançar. Mesmo que uma verificação no código falhe, os sistemas internos continuam inacessíveis. Além disso, dificulta, após uma intrusão, o descarregamento de código malicioso e a fuga de dados.