En bref : vous empêchez le SSRF en PHP en veillant à ce que votre application n'appelle que des adresses figurant sur une liste autorisée fixe, exclusivement en HTTPS, sans suivre les redirections. L'adresse IP résolue est contrôlée par rapport aux plages privées et réservées, puis figée pour la connexion. Un pare-feu pour le trafic sortant garantit que les systèmes internes restent inaccessibles même en cas d'erreur dans le code.
Quand votre propre serveur envoie des requêtes pour des tiers
Aperçus de liens dans un chat, import d'une image de produit par URL, webhooks vers des systèmes partenaires, lecteur de flux dans le CMS : de nombreuses applications PHP appellent des adresses qu'un utilisateur a indiquées. C'est une fonction utile et, en même temps, une porte vers l'intérieur.
Avec le Server-Side Request Forgery, en abrégé SSRF, un attaquant amène l'application à envoyer une requête vers une cible de son choix. Le point clé : la requête vient du serveur lui-même. Elle atteint donc des systèmes fermés depuis l'extérieur, par exemple des bases de données, des interfaces d'administration du réseau interne, des services qui n'écoutent que sur localhost, ou les services de métadonnées des fournisseurs de cloud.
Dans l'OWASP Top 10 de 2021, le SSRF formait une catégorie à part. Dans les projets PHP, la faille apparaît souvent là où quelqu'un a rapidement construit une fonction « ajouter une image par URL ».
SSRF en PHP : quelles fonctions appellent des adresses
Ce n'est pas seulement cURL qui appelle des adresses. Gardez ces points à l'œil :
| Fonction ou domaine | Pourquoi c'est pertinent |
|---|---|
cURL (curl_exec()) |
voie la plus fréquente, de nombreux protocoles possibles |
file_get_contents(), fopen(), copy() avec une URL |
utilisent des wrappers d'URL lorsque allow_url_fopen est actif |
getimagesize() et bibliothèques d'images |
peuvent charger des fichiers distants |
| Traitement XML, clients SOAP | chargent éventuellement des ressources externes |
| Bibliothèques HTTP comme Guzzle | reposent sur cURL ou les flux, mêmes règles |
Si votre application n'a pas besoin d'ouvrir des fichiers distants via les fonctions de fichiers, désactivez allow_url_fopen côté serveur. Un sujet voisin, l'inclusion de fichiers via des chemins issus de la requête, est traité dans le guide Empêcher les inclusions de fichiers locales et distantes.
La défense en cinq étapes
L'OWASP recommande une combinaison de mesures qui, prises isolément, pourraient chacune être contournées, mais qui tiennent ensemble :
- Liste autorisée au lieu d'une URL libre : le mieux est que l'utilisateur choisisse seulement parmi des cibles connues, ou qu'il n'indique qu'un identifiant et que l'application construise elle-même l'adresse.
- N'autoriser que HTTPS : exclure dans cURL les autres protocoles comme
file,gopherouftp, y compris pour les redirections. - Ne pas suivre les redirections : une adresse autorisée peut sinon rediriger vers une cible interne.
- Contrôler et figer l'adresse résolue : résoudre le nom d'hôte, vérifier l'IP par rapport aux plages privées, de bouclage (loopback) et de lien local, et utiliser exactement cette IP pour la connexion. Sinon, une seconde résolution DNS peut renvoyer une autre cible.
- Limiter le réseau : le serveur web ne doit pouvoir atteindre vers l'extérieur que les cibles dont il a besoin.
Un modèle sûr avec cURL
L'exemple suivant met en œuvre les étapes 1 à 4 pour IPv4. Les noms d'hôte et le déroulement sont des exemples ; les mêmes contrôles valent pour IPv6.
<?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;
}
Quelques détails sont importants. gethostbyname() renvoie le nom d'hôte en cas d'erreur ; le contrôle d'IP qui suit l'intercepte. Selon le manuel PHP, les indicateurs FILTER_FLAG_NO_PRIV_RANGE et FILTER_FLAG_NO_RES_RANGE excluent notamment les plages 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8 et 169.254.0.0/16. Depuis PHP 8.2, il existe en plus FILTER_FLAG_GLOBAL_RANGE, qui n'admet que des adresses globalement valides. CURLOPT_RESOLVE fait en sorte que cURL utilise exactement l'adresse contrôlée, tandis que le nom d'hôte et la vérification du certificat sont conservés.
Deux autres points limitent ce qu'un attaquant apprend d'une faille SSRF. Ne renvoyez pas à l'utilisateur, sans filtrage, les réponses et messages d'erreur de la cible appelée : la seule distinction entre « connexion refusée » et « dépassement de délai » révèle quels services internes existent. Et limitez la durée et la taille de la réponse, afin que la fonction ne puisse pas servir à surcharger votre propre serveur. Pour les webhooks vers des adresses que les clients saisissent eux-mêmes, envoyez-les de préférence depuis une file d'attente plutôt que pendant la requête en cours.
Si votre application doit vraiment appeler des adresses quelconques, par exemple pour des aperçus de liens, externalisez cette fonction dans un service distinct sans accès au réseau interne. Une liste autorisée n'est alors pas possible, et la séparation des réseaux n'en est que plus importante.
Le niveau réseau comme filet de sécurité
Les contrôles dans le code peuvent comporter des failles, par exemple lorsque deux bibliothèques décomposent une URL différemment. L'OWASP conseille de traiter de telles incohérences comme un rejet et de limiter en plus au niveau du réseau. Un serveur web n'a généralement besoin vers l'extérieur que de quelques cibles : sources de paquets, prestataires de paiement et d'expédition, relais de messagerie, certaines interfaces.
Un pare-feu pour le trafic sortant aide doublement. Il arrête les requêtes SSRF vers le réseau interne, et il complique, après une intrusion, par exemple via une version de PHP non corrigée, le téléchargement d'autres outils. La manière de construire les règles est décrite dans Pare-feu pour serveur web : n'ouvrir que les ports nécessaires. Les connexions qu'un serveur établit réellement sont présentées dans Vérifier les connexions sortantes d'un serveur.
Dans les environnements cloud, la variante durcie du service de métadonnées en fait partie. Pour AWS, l'OWASP cite le passage à IMDSv2 et la désactivation d'IMDSv1.
Repérer le SSRF dans le code existant
Une entrée rapide consiste à rechercher les endroits où des fonctions d'appel reçoivent des valeurs de la requête :
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"
Les résultats sont des tâches de vérification. Examinez tout particulièrement les fonctions que les administrateurs utilisent : imports, réglages de webhooks, vérifications de mises à jour des extensions.
Les générateurs de PDF, qui convertissent du HTML avec des images ou feuilles de style intégrées en documents, sont facilement négligés. Si une partie de ce HTML provient de saisies d'utilisateurs, par exemple pour des factures avec un logo au choix, le générateur appelle éventuellement des adresses que l'utilisateur détermine. Vérifiez-y les réglages de chargement des ressources distantes et désactivez-le s'il n'est pas nécessaire.
Comment PHP SOS aide-t-il concrètement ?
- Nous trouvons tous les endroits où votre application appelle des adresses, y compris dans les extensions et les bibliothèques.
- Nous construisons une fonction d'appel centrale avec liste autorisée, restriction des protocoles et contrôle d'IP.
- Nous définissons des règles pour le trafic sortant et protégeons les services de métadonnées dans les environnements cloud.
- Nous vérifions la mise en œuvre depuis l'extérieur et documentons les cibles autorisées.
Un contrôle indépendant de votre application est décrit dans audit de sécurité et test d'intrusion.
Questions fréquentes
Quelles fonctions PHP peuvent être détournées pour un SSRF ?
Toutes celles qui appellent une adresse issue d'une saisie d'utilisateur : cURL, file_get_contents et fopen avec des wrappers d'URL, les fonctions d'image et de XML qui chargent des fichiers distants, ainsi que les bibliothèques pour webhooks, flux ou aperçus de liens.
Suffit-il de bloquer les adresses IP internes ?
Pas à lui seul. L'OWASP qualifie les listes de blocage de contournables et recommande des listes autorisées. En outre, l'application doit utiliser exactement l'adresse qu'elle a contrôlée et ne doit pas suivre les redirections.
Pourquoi le SSRF est-il particulièrement critique dans le cloud ?
Les fournisseurs de cloud mettent à la disposition des instances des services de métadonnées internes, via lesquels des identifiants sont parfois accessibles. Si une application peut leur envoyer des requêtes, un attaquant peut atteindre ces données par son intermédiaire. Les fournisseurs proposent des variantes durcies de ces services, qui devraient être activées.
À quoi sert un pare-feu pour le trafic sortant ?
Il limite les destinations que le serveur peut atteindre. Même si un contrôle échoue dans le code, les systèmes internes restent alors inaccessibles. Il complique en outre, après une intrusion, le téléchargement de code malveillant et l'exfiltration de données.