Le serveur va chercher l'adresse que vous lui donnez
La leçon précédente montrait comment un attaquant fait entrer un fichier malveillant par la porte de devant. Le SSRF retourne le problème : cette fois, c'est le serveur lui-même qu'on pousse à sortir là où il ne devrait pas aller.
Beaucoup de sites ont une fonction « va chercher cette URL pour moi » : un aperçu de lien, un import d'image depuis une adresse, un webhook, un générateur de PDF qui charge des ressources. Le code est tout simple :
$contenu = file_get_contents($_GET['url']); // le serveur va chercher l'URL
echo $contenu;
L'attaquant ne donne pas l'URL d'une image. Il donne une adresse interne, que lui ne peut pas joindre, mais que le serveur, lui, peut atteindre :
?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
Pourquoi cette adresse précise ? Chaque machine virtuelle chez AWS, GCP ou Azure peut interroger 169.254.169.254 : c'est une adresse réservée, visible uniquement de l'intérieur, comme un badge que seul le salarié peut demander au guichet. De l'extérieur, elle est invisible.
Cette adresse 169.254.169.254 est le service de métadonnées du cloud (AWS, GCP, Azure). Depuis l'intérieur, il livre les identifiants temporaires du serveur. Le serveur les récupère… et vous les renvoie. L'attaquant repart avec les clés du compte cloud.
C'est la SSRF (Server-Side Request Forgery) : on force votre serveur à émettre une requête vers une cible que l'attaquant choisit. Le serveur devient un intermédiaire de confiance qui frappe à des portes interdites. C'est un contrôle d'accès contourné côté serveur (catégorie A10 du Top 10 OWASP 2021, toujours au cœur des risques cloud).
Pourquoi ça marche
La force de l'attaque tient à un déséquilibre : le serveur voit le réseau interne, l'attaquant non. De l'extérieur, impossible de joindre localhost, une base de données privée ou le panneau d'admin sur 192.168.x.x. Mais si l'attaquant fait requêter le serveur à sa place, il emprunte sa position de confiance et atteint tout ce que le serveur peut atteindre.
Les cibles classiques :
- Les métadonnées cloud
169.254.169.254: identifiants temporaires, souvent la clé du compte entier. - localhost et le réseau privé :
127.0.0.1,10.0.0.0/8,192.168.0.0/16: bases, files d'attente, admins internes non exposés. - Les autres schémas d'URL :
file:///etc/passwd(lire un fichier),gopher://(parler à Redis ou un SMTP interne).
Le faux remède : « je bloque localhost et 127.0.0.1 » est une blacklist, et elle se contourne en quelques caractères. 127.0.0.1 s'écrit aussi 2130706433 (décimal), 127.1, 0177.0.0.1 (octal), [::1] (IPv6). Pire : un domaine contrôlé par l'attaquant peut résoudre vers 127.0.0.1. Et une URL pourtant autorisée peut renvoyer une redirection 302 vers l'adresse interne. La vraie parade ne filtre pas du texte : elle résout l'adresse et bloque les plages privées (section 4).
À vous d'attaquer : atteignez le réseau interne
Voici un aperçu d'URL vraiment vulnérable, simulé dans votre navigateur. Le serveur bloque naïvement localhost et 127.0.0.1. Trouvez une autre façon d'atteindre l'intérieur, jusqu'aux identifiants cloud. Puis activez la version sécurisée. Tout est simulé, aucune requête réseau réelle.
Bloqué ? Voir la solution
Le filtre ne connaît que les mots localhost et 127.0.0.1. Contournez-le :
http://2130706433/ ou http://127.1/ atteignent quand même 127.0.0.1 ; et surtout http://169.254.169.254/latest/meta-data/iam/security-credentials/ n'est même pas dans la blacklist : c'est le jackpot, les identifiants cloud. En version sécurisée, le serveur résout l'adresse et refuse toute plage privée ou lien-local : aucune de ces URL ne passe.
Le correctif : résoudre l'adresse, refuser le privé
On ne filtre pas le texte de l'URL : on résout l'adresse réelle et on vérifie qu'elle pointe vers une destination publique autorisée. L'idéal reste une allowlist des domaines que votre application a vraiment besoin de joindre.
$hotes_ok = ['images.partenaire.com', 'cdn.exemple.com'];
$url = $_GET['url'];
$host = parse_url($url, PHP_URL_HOST);
// 1. schéma autorisé (http/https seulement) et hôte dans l'allowlist
if (!in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)) { exit; }
if (!in_array($host, $hotes_ok, true)) { http_response_code(403); exit; }
// 2. résoudre l'IP réelle et refuser loopback / privé / lien-local
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
http_response_code(403); exit; // 127.x, 10.x, 192.168.x, 169.254.x → bloqués
}
La redirection et au DNS. Vérifier l'URL de départ ne suffit pas : une page autorisée peut renvoyer une redirection 302 vers 169.254.169.254, et un domaine peut changer de résolution DNS entre la vérification et la requête (DNS rebinding). Il faut donc désactiver les redirections (ou revérifier chaque saut) et idéalement passer par un proxy de sortie qui ne route que vers l'extérieur.
Défense en profondeur :
- allowlist des hôtes et des schémas (
http/httpsuniquement, jamaisfile:///gopher://) ; - résoudre l'IP et bloquer loopback, plages privées et lien-local (
169.254.x) ; - désactiver les redirections, ou revalider l'IP à chaque saut ;
- ne pas renvoyer la réponse brute à l'utilisateur (limite la fuite) ;
- côté cloud : IMDSv2 (jeton requis) et politiques réseau qui isolent le service.
Référence : OWASP SSRF Prevention Cheat Sheet.
La méthode et l'arsenal du pentester
On vient de voir comment se défendre. Voyons maintenant comment un attaquant trouve et exploite la même faille, pour mieux comprendre ce qu'on protège.
1. Repérer les fonctions qui requêtent. Aperçu de lien, import « depuis une URL », webhook, génération de PDF/capture, validation d'avatar distant, intégration tierce : partout où le serveur va chercher une adresse fournie.
2. Viser l'interne. On pointe vers 127.0.0.1, les plages privées, et surtout 169.254.169.254 (métadonnées cloud). Si la réponse change ou révèle du contenu interne, la SSRF est confirmée.
3. Contourner et escalader. Contre un filtre, on essaie les encodages d'IP (2130706433, octal), un domaine qui résout en interne, ou une redirection. Quand la réponse n'est pas renvoyée (SSRF aveugle), on confirme par canal hors-bande (le serveur appelle une URL qu'on surveille). Puis on escalade : vol des identifiants cloud, scan de ports interne, ou RCE via gopher:// sur un Redis.
L'arsenal.
- Burp Suite + Collaborator : détecter la SSRF aveugle par interaction hors-bande.
- SSRFmap / Gopherus : automatiser l'exploitation et forger des charges
gopher://vers les services internes. - interactsh : serveur d'interaction hors-bande open source.
L'impact
La SSRF est devenue critique avec le cloud. L'exemple emblématique : la fuite Capital One de 2019 : une SSRF a permis d'atteindre les métadonnées AWS, d'en extraire des identifiants, puis de lire plus de 100 millions de dossiers clients. Une fonction « aperçu d'URL » a ouvert tout un compte cloud.
Au-delà des identifiants, la SSRF transforme votre serveur en sonde du réseau interne : scan de ports pour cartographier les services privés, accès aux panneaux d'admin et bases non exposés, et parfois exécution de code en parlant à un Redis ou un SMTP interne via gopher://. Une seule entrée d'URL ouvre la porte à tout ce qui était « protégé » par le simple fait d'être interne.
Ce que ça révèle côté défense : « interne donc protégé » est une illusion. Un service ne doit pas faire confiance à une requête simplement parce qu'elle vient d'un autre service interne. On valide la destination (allowlist + résolution + plages privées bloquées), on durcit le cloud (IMDSv2), et on segmente le réseau. La confiance se vérifie, même entre vos propres machines (leçon 1).
La checklist SSRF
À vérifier sur toute fonction où le serveur va chercher une URL fournie.
- Allowlist des hôtes et des schémas (
http/httpsseulement). - Résolution de l'IP et blocage des plages loopback / privées / lien-local.
- Redirections désactivées ou revalidées à chaque saut.
- Réponse non renvoyée brute à l'utilisateur.
- Cloud durci : IMDSv2 et segmentation réseau.
Les références. L'OWASP SSRF Prevention Cheat Sheet liste chaque parade et chaque contournement. Pour s'entraîner : les labs SSRF de la Web Security Academy.
Rappel. Faire requêter un serveur qui n'est pas le vôtre vers des cibles internes est une intrusion. On ne teste que ses propres systèmes ou une cible explicitement autorisée (leçon 1 du cours principal).
The server fetches the address you hand it
The previous lesson showed how an attacker makes a malicious file enter through the front door. SSRF flips the problem: this time, it's the server itself that gets pushed to go out where it shouldn't.
Many sites have a "fetch this URL for me" feature: a link preview, an image import from an address, a webhook, a PDF generator loading resources. The code is dead simple:
$content = file_get_contents($_GET['url']); // the server fetches the URL
echo $content;
The attacker doesn't give an image URL. They give an internal address, one they can't reach themselves, but the server can:
?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
Why that specific address? Every virtual machine at AWS, GCP, or Azure can query 169.254.169.254: it's a reserved address, visible only from the inside — like a badge only an employee can request at the front desk. From outside, it's invisible.
That address 169.254.169.254 is the cloud metadata service (AWS, GCP, Azure). From the inside, it hands out the server's temporary credentials. The server fetches them… and returns them to you. The attacker walks away with the keys to the cloud account.
That's SSRF (Server-Side Request Forgery): you force your server to send a request to a target the attacker chooses. The server becomes a trusted middleman knocking on forbidden doors. It's a server-side access control bypass (category A10 of the OWASP Top 10 2021, still at the heart of cloud risk).
Why it works
The attack's power comes from an imbalance: the server sees the internal network, the attacker doesn't. From outside, you can't reach localhost, a private database, or the admin panel on 192.168.x.x. But if the attacker makes the server request on their behalf, they borrow its trusted position and reach everything the server can reach.
The classic targets:
- Cloud metadata
169.254.169.254: temporary credentials, often the key to the whole account. - localhost and the private network:
127.0.0.1,10.0.0.0/8,192.168.0.0/16— databases, queues, internal admins not exposed. - Other URL schemes:
file:///etc/passwd(read a file),gopher://(talk to Redis or an internal SMTP).
The false cure: "I block localhost and 127.0.0.1" is a blacklist, and it's bypassed in a few characters. 127.0.0.1 is also written 2130706433 (decimal), 127.1, 0177.0.0.1 (octal), [::1] (IPv6). Worse: a domain the attacker controls can resolve to 127.0.0.1, or an allowed URL can return a 302 redirect to the internal address. The real fix doesn't filter text: it resolves the address and blocks private ranges (section 4).
Your turn to attack: reach the internal network
Here's a genuinely vulnerable URL preview, simulated in your browser. The server naively blocks localhost and 127.0.0.1. Find another way to reach the inside, up to the cloud credentials. Then switch on the secure version. Everything is simulated, no real network request.
Stuck? Show the solution
The filter only knows the words localhost and 127.0.0.1. Bypass it:
http://2130706433/ or http://127.1/ still reach 127.0.0.1; and above all http://169.254.169.254/latest/meta-data/iam/security-credentials/ isn't even in the blacklist: that's the jackpot, the cloud credentials. In the secure version, the server resolves the address and refuses any private or link-local range: none of these URLs get through.
The fix: resolve the address, refuse the private
You don't filter the URL text: you resolve the real address and check it points to an allowed public destination. The ideal is still an allowlist of the domains your app actually needs to reach.
$ok_hosts = ['images.partner.com', 'cdn.example.com'];
$url = $_GET['url'];
$host = parse_url($url, PHP_URL_HOST);
// 1. allowed scheme (http/https only) and host in the allowlist
if (!in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)) { exit; }
if (!in_array($host, $ok_hosts, true)) { http_response_code(403); exit; }
// 2. resolve the real IP and refuse loopback / private / link-local
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
http_response_code(403); exit; // 127.x, 10.x, 192.168.x, 169.254.x → blocked
}
Beware redirects and DNS. Checking the starting URL isn't enough: an allowed page can return a 302 redirect to 169.254.169.254, and a domain can change its DNS resolution between the check and the request (DNS rebinding). So you must disable redirects (or re-check every hop) and ideally go through an egress proxy that only routes outward.
Defense in depth:
- allowlist of hosts and schemes (
http/httpsonly, neverfile:///gopher://); - resolve the IP and block loopback, private ranges and link-local (
169.254.x); - disable redirects, or re-validate the IP at each hop;
- don't return the raw response to the user (limits leakage);
- cloud side: IMDSv2 (token required) and network policies isolating the service.
Reference: OWASP SSRF Prevention Cheat Sheet.
The pentester's method and arsenal
We've just covered the defenses. Now let's see how an attacker finds and exploits the same flaw — so you truly understand what you're protecting.
1. Spot the requesting features. Link preview, "from a URL" import, webhook, PDF/screenshot generation, remote avatar validation, third-party integration: anywhere the server fetches a supplied address.
2. Aim inward. You point at 127.0.0.1, the private ranges, and above all 169.254.169.254 (cloud metadata). If the response changes or reveals internal content, the SSRF is confirmed.
3. Bypass and escalate. Against a filter, you try IP encodings (2130706433, octal), a domain that resolves internally, or a redirect. When the response isn't returned (blind SSRF), you confirm via an out-of-band channel (the server calls a URL you watch). Then you escalate: cloud credential theft, internal port scan, or RCE via gopher:// on a Redis.
The arsenal.
- Burp Suite + Collaborator: detect blind SSRF through out-of-band interaction.
- SSRFmap / Gopherus: automate exploitation and forge
gopher://payloads to internal services. - interactsh: open-source out-of-band interaction server.
The impact
SSRF became critical with the cloud. The emblematic case: the Capital One breach of 2019 — an SSRF reached the AWS metadata, extracted credentials, then read over 100 million customer records. A "URL preview" feature opened a whole cloud account.
Beyond credentials, SSRF turns your server into a probe of the internal network: port scanning to map private services, access to unexposed admin panels and databases, and sometimes code execution by talking to an internal Redis or SMTP via gopher://. A single URL input opens the door to everything that was "protected" merely by being internal.
What this reveals for defense: "internal therefore protected" is an illusion. A service shouldn't trust a request just because it comes from another internal service. You validate the destination (allowlist + resolution + private ranges blocked), harden the cloud (IMDSv2), and segment the network. Trust is verified, even between your own machines (lesson 1).
The SSRF checklist
To check on every feature where the server fetches a supplied URL.
- Allowlist of hosts and schemes (
http/httpsonly). - IP resolution and blocking of loopback / private / link-local ranges.
- Redirects disabled or re-validated at each hop.
- Raw response not returned to the user.
- Hardened cloud: IMDSv2 and network segmentation.
The references. The OWASP SSRF Prevention Cheat Sheet lists every fix and every bypass. To practice: the SSRF labs of the Web Security Academy.
Reminder. Making a server that isn't yours request internal targets is an intrusion. Only test your own systems or an explicitly authorized target (lesson 1 of the main course).
Une appli refuse les URL contenant localhost ou 127.0.0.1, puis va chercher l'URL côté serveur. Avant de dérouler : l'attaquant peut-il encore atteindre les métadonnées cloud à 169.254.169.254 ?
Voir la réponse
Oui, sans aucun effort. Le filtre ne bloque que deux mots ; 169.254.169.254 n'en fait pas partie. Et même pour viser 127.0.0.1, il existe 2130706433, 127.1, [::1]. Une blacklist de chaînes ne voit jamais toutes les écritures d'une même adresse. La vraie parade résout l'IP et refuse toute plage privée ou lien-local, quelle que soit la façon dont l'URL est écrite.
An app refuses URLs containing localhost or 127.0.0.1, then fetches the URL server-side. Before you expand: can the attacker still reach the cloud metadata at 169.254.169.254?
Show the answer
Yes, with no effort at all. The filter blocks only two words; 169.254.169.254 isn't one of them. And even to hit 127.0.0.1, there's 2130706433, 127.1, [::1]. A string blacklist never sees every way of writing the same address. The real fix resolves the IP and refuses any private or link-local range, however the URL is written.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
💬 Ré-explique sans regarder
Avec tes mots : qu'est-ce qu'une SSRF (qui requête quoi ?), pourquoi 169.254.169.254 est si dangereux, et pourquoi bloquer localhost ne protège pas ?
localhost, les bases privées, les admins internes, et surtout 169.254.169.254, le service de métadonnées cloud qui livre les identifiants du compte. Bloquer localhost/127.0.0.1 est une blacklist : on contourne avec 2130706433, 127.1, [::1], un domaine qui résout en interne, ou une redirection, et 169.254.169.254 n'est souvent même pas filtré. La vraie parade ne filtre pas le texte : elle résout l'IP réelle et refuse loopback/privé/lien-local, avec une allowlist d'hôtes et sans suivre les redirections.🧠 Rappel libre
Sans remonter : explique ce qu'est une SSRF, pourquoi 169.254.169.254 est la cible reine, pourquoi une blacklist d'adresses échoue, et la parade (résoudre l'IP + bloquer le privé).
localhost, bases et admins privés, et 169.254.169.254, les métadonnées cloud, qui livrent les identifiants du compte (fuite Capital One 2019). Une blacklist échoue car une même adresse s'écrit de mille façons (2130706433, 127.1, [::1]), sans compter les redirections et le DNS rebinding. Parade : allowlist d'hôtes/schémas, résoudre l'IP réelle et refuser loopback/privé/lien-local, désactiver les redirections, durcir le cloud (IMDSv2).⚖️ Juge le code de l'IA
Tu demandes à l'IA de protéger ta fonction d'aperçu d'URL. Elle répond : « J'ajoute un filtre : je refuse l'URL si elle contient localhost, 127.0.0.1 ou 169.254. Le reste passe. » Tu acceptes, ou tu rejettes ?
127.0.0.1 s'écrit 2130706433, 127.1, [::1] ; 169.254.169.254 a aussi des écritures alternatives ; et un domaine que l'attaquant contrôle peut résoudre vers une IP interne (ou rediriger en 302 vers elle) sans jamais contenir un mot interdit. Filtrer le texte de l'URL perd toujours. La bonne approche valide la destination réelle : on résout le nom d'hôte en IP, on refuse loopback / privé / lien-local, on s'appuie sur une allowlist d'hôtes autorisés, et on ne suit pas les redirections (ou on revalide à chaque saut).169.254.169.254 est la cible préférée d'une SSRF en cloud ?127.0.0.1 ne suffit pas ?Vous avez fait requêter le serveur où vous vouliez. La leçon suivante le fait carrément exécuter votre code : l'injection de template (SSTI), quand un moteur de rendu prend vos données pour des instructions.
Leçon 4 : Injection de template (SSTI) →