Le serveur récupère une URL fournie par l'attaquant, exposant les services internes, les endpoints de métadonnées cloud ou les ressources localhost.
TL;DR
url, webhook, callback, image, redirect, srcAccessKeyId, SecretAccessKey, TokenLe SSRF basique (en bande) est la forme la plus simple et la plus immédiatement impactante de CWE-918. Une application accepte une URL depuis une entrée utilisateur, effectue une requête HTTP côté serveur vers cette URL, et retourne la réponse — ou une portion de celle-ci — directement dans la réponse HTTP. L'attaquant voit la réponse de la cible interne comme si le serveur était un proxy transparent.
La caractéristique distinctive est la visibilité : contrairement au SSRF aveugle, où la réponse est supprimée, le SSRF basique retourne le contenu récupéré. Cela le rend trivialement exploitable pour le vol de credentials cloud et la reconnaissance interne sans nécessiter d'infrastructure hors-bande. OWASP A10:2021 identifie ce pattern comme l'une des vulnérabilités web à l'impact le plus élevé précisément parce que l'endpoint de métadonnées cloud (169.254.169.254) est accessible par défaut depuis toute instance AWS, GCP ou Azure.
Le serveur accepte une entrée utilisateur contenant une URL, construit une requête HTTP, et reflète la réponse. L'attaque tire parti de la position réseau de confiance du serveur : les services internes qui retourneraient 403 Forbidden ou seraient inaccessibles depuis internet répondent normalement aux requêtes provenant de l'IP du serveur.
La chaîne d'attaque :
url, src, image, callback, webhook, redirect, import, etc.)http://127.0.0.1/, http://169.254.169.254/, http://192.168.1.1:8080/admin/)Le pattern de code vulnérable en Python :
# VULNÉRABLE — récupération directe d'URL contrôlée par l'utilisateur
import requests
def fetch_preview(url: str) -> dict:
# url = "http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE"
resp = requests.get(url, timeout=10, allow_redirects=True)
return {"content": resp.text, "status": resp.status_code}
# L'attaquant reçoit les credentials IAM dans la réponse JSON| Technique | Cible | Exemple de payload | Impact |
|---|---|---|---|
| Métadonnées cloud — IMDSv1 | AWS 169.254.169.254 | http://169.254.169.254/latest/meta-data/iam/security-credentials/ | Vol de credentials IAM |
| Métadonnées cloud — GCP | metadata.google.internal | http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token | Token de compte de service GCP |
| Panel d'administration localhost | UI admin interne | http://127.0.0.1:8080/admin/ | Contournement d'accès admin |
| Scan de service interne | Redis, Elasticsearch | http://127.0.0.1:9200/_cat/indices | Exfiltration de données |
| API interne | Microservice | http://internal-api.svc.cluster.local/v1/users | Accès aux données métier |
| Dépliage de lien | Scrapers OG/meta | http://169.254.169.254/ | Credentials cloud via application de chat |
Le dépliage de lien est une surface d'attaque masquée à haute valeur : les applications qui génèrent des aperçus d'URL (Slack, Discord, cartes d'aperçu CMS) effectuent toutes des requêtes côté serveur vers des URLs fournies par l'attaquant.
Capital One 2019 — La brèche SSRF canonique. Un produit WAF déployé sur une instance EC2 avait une mauvaise configuration permettant le SSRF. L'attaquant a soumis des requêtes HTTP contenant l'URL de métadonnées AWS IMDSv1. Le WAF a effectué la requête depuis son IP EC2 interne, retourné le nom du rôle IAM, puis l'attaquant a récupéré le JSON de credentials : {"AccessKeyId":"ASIAJWNJSXFH...","SecretAccessKey":"...","Token":"...","Expiration":"2019-03-18T12:54:10Z"}. Le rôle ISRM-WAF-Role sur-privilégié avait un accès GetObject sur 700+ buckets. Environ 30 Go de données ont été exfiltrés — 100 millions de dossiers américains et 6 millions de dossiers canadiens. À la divulgation (2024), seulement 32 % des instances EC2 avaient IMDSv2 appliqué.
CVE-2024-8977 — GitLab EE (CVSS 8.2, CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N) — Le tableau de bord Product Analytics de GitLab effectuait des requêtes HTTP côté serveur vers des endpoints Cube API configurables. Tout utilisateur authentifié pouvait remplacer l'URL de l'endpoint Cube par http://169.254.169.254/latest/meta-data/ et le tableau de bord affichait la réponse interne brute. Corrigé dans GitLab 17.4.2, 17.3.5, 17.2.9 (octobre 2024). Signalé via HackerOne par joaxcar.
CVE-2025-68437 — Craft CMS — La mutation GraphQL saveAssets acceptait des URLs arbitraires dans _file { url } et les récupérait côté serveur. Le contenu récupéré était enregistré comme asset et téléchargeable via l'API du CMS — transformant une fonctionnalité d'upload standard en SSRF en bande à lecture complète. Les plateformes CMS qui permettent l'import d'assets depuis URL sont une surface d'attaque constamment sous-estimée.
Les boutons de test webhook sont un vecteur SSRF fréquemment négligé. Quand une plateforme vous permet de tester une URL webhook en effectuant une requête côté serveur et en affichant la réponse, c'est un SSRF en bande. Le rapport HackerOne #2301565 a rapporté 2 500 $ pour exactement ce pattern.
url, src, href, callback, webhook, endpoint, redirect, image, avatar, import_url, feed, resource), et en-têtes HTTP (Host, X-Forwarded-Host, Referer).http://127.0.0.1/ et comparer la réponse à une requête de contrôle avec une URL légitime. Une différence de contenu, longueur ou statut confirme que le serveur a effectué une requête sortante.http://169.254.169.254/latest/meta-data/ — si la réponse contient ami-id, instance-id, ou des données de rôle IAM, l'application est dans un environnement cloud et les credentials sont directement accessibles.http://127.0.0.1:8080/admin/, http://localhost:3000/, http://127.0.0.1:9200/ (Elasticsearch).# Burp Repeater — test SSRF basique
GET /api/image-preview?url=http://169.254.169.254/latest/meta-data/ HTTP/1.1
Host: target.example.com
# Réponse vulnérable attendue :
# ami-id
# ami-launch-index
# hostname
# iam/
# ... (liste de métadonnées)Nuclei inclut des templates SSRF qui testent les noms de paramètres courants contre les endpoints de métadonnées cloud. SSRFmap automatise le fuzzing multi-paramètres avec des payloads cloud-spécifiques. Burp Scanner Pro identifie les paramètres acceptant des URLs et teste les callbacks OOB via Collaborator.
BreachVex analyse 80+ noms de paramètres SSRF canoniques plus la détection basée sur la valeur (tout paramètre dont la valeur correspond à https?:// ou IPv4 nu), lance des sondes de métadonnées cloud pour AWS/GCP/Azure, et mesure la réponse différentielle par rapport aux IPs de contrôle RFC 5737 (192.0.2.1) pour confirmer l'exécution des requêtes sans nécessiter de réflexion de réponse.
import ipaddress
import socket
import urllib.parse
from typing import Optional
ALLOWED_HOSTS = frozenset({"api.stripe.com", "hooks.slack.com", "cdn.example.com"})
class SSRFError(ValueError):
pass
def validate_and_fetch(url: str) -> str:
parsed = urllib.parse.urlparse(url)
# Liste d'autorisation de schémas — rejette gopher://, file://, dict://, ftp://
if parsed.scheme not in ("http", "https"):
raise SSRFError(f"Schéma non autorisé : {parsed.scheme}")
host = (parsed.hostname or "").rstrip(".") # supprimer le point FQDN final
if not host:
raise SSRFError("Hostname manquant")
# Liste d'autorisation exacte de hosts — pas contains/startsWith
if host not in ALLOWED_HOSTS:
raise SSRFError(f"Host absent de la liste d'autorisation : {host}")
# Résoudre DNS et valider TOUTES les adresses retournées (contrecarre les alias DNS + nip.io)
try:
results = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP)
for (_, _, _, _, sockaddr) in results:
ip = ipaddress.ip_address(sockaddr[0])
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
ip = ip.ipv4_mapped # normaliser ::ffff:169.254.169.254
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
raise SSRFError(f"Résolution vers une adresse bloquée : {ip}")
except (socket.gaierror, ValueError) as e:
raise SSRFError(f"Validation DNS échouée : {e}")
import requests
# Désactiver le suivi des redirections — valider l'en-tête Location séparément
resp = requests.get(url, timeout=5, allow_redirects=False)
if resp.is_redirect:
raise SSRFError(f"Redirection vers une URL non validée : {resp.headers.get('location')}")
return resp.textNe pas retourner les réponses internes brutes. Si la fonctionnalité n'a besoin que d'un titre ou d'une image, extraire le champ spécifique et rejeter le reste :
# VULNÉRABLE — retourne la réponse interne brute
return {"preview": requests.get(url).text}
# SÛR — extrait uniquement le champ nécessaire
from bs4 import BeautifulSoup
soup = BeautifulSoup(requests.get(validated_url, allow_redirects=False).text, "html.parser")
title = soup.find("title")
return {"preview_title": title.text[:200] if title else ""}La validation par résolution DNS doit s'effectuer au moment de la requête, pas au moment de l'enregistrement. Un attaquant peut enregistrer trusted.example.com dans votre liste d'autorisation quand il résout vers une IP légitime, puis changer l'enregistrement DNS vers 169.254.169.254. Valider l'IP résolue pour chaque requête — c'est ce qu'on appelle l'épinglage DNS.
Tout paramètre dont la valeur est utilisée dans une requête HTTP côté serveur est un candidat : url, src, href, redirect, callback, webhook, image, avatar, import_url, feed, endpoint, et 80+ noms canoniques. La détection basée sur la valeur repère les champs non étiquetés : tout paramètre dont la valeur courante correspond à http://, // ou un pattern IPv4 nu est un candidat SSRF quel que soit son nom.
Dans le SSRF basique (en bande), le serveur retourne le corps de la réponse à l'attaquant — le contenu de la ressource interne est visible dans la réponse HTTP. Dans le SSRF aveugle, le serveur effectue la requête mais supprime la réponse ; la confirmation nécessite un callback hors-bande (hit DNS/HTTP Burp Collaborator, Interactsh).
Tout service accessible depuis le réseau du serveur : endpoints de métadonnées cloud (169.254.169.254), panels d'administration, Redis sur 6379, Elasticsearch sur 9200, Kubernetes API sur 6443, Docker API sur 2375, etcd sur 2379, tableaux de bord de monitoring internes, et tout microservice sur le réseau interne non exposé à internet.
Étape 1 : récupérer http://169.254.169.254/latest/meta-data/iam/security-credentials/ pour obtenir le nom du rôle IAM. Étape 2 : récupérer http://169.254.169.254/latest/meta-data/iam/security-credentials/{NOM_ROLE} pour obtenir AccessKeyId, SecretAccessKey, Token. Étape 3 : utiliser les credentials avec AWS CLI pour énumérer les buckets S3, les instances EC2 ou d'autres ressources.
En 2019, un WAF mal configuré sur EC2 avait une vulnérabilité SSRF basique. L'attaquant a interrogé 169.254.169.254 directement, obtenu des credentials IAM pour le rôle sur-privilégié ISRM-WAF-Role, et les a utilisés pour accéder à 700+ buckets S3. La brèche a exposé 106 millions de dossiers et a entraîné une amende OCC de 80M$. La réponse était visible en bande — SSRF basique classique.
Le tableau de bord Product Analytics de GitLab EE effectuait des requêtes HTTP côté serveur vers des endpoints Cube API configurables. Les utilisateurs authentifiés pouvaient remplacer l'URL d'endpoint par des adresses internes. La réponse était retournée à l'UI, en faisant un SSRF en bande complet. CVSS 8.2, corrigé dans GitLab 17.4.2.
Plutôt que de faire correspondre les noms de paramètres, la détection basée sur la valeur identifie les paramètres dont la valeur courante contient déjà un pattern URL (https?://, //, ou IPv4 nu). Cela repère les cas où le paramètre est nommé 'data', 'config', ou 'settings' mais la valeur est 'http://internal:9200'. BreachVex utilise à la fois la détection basée sur le nom et sur la valeur.
Maintenir une liste d'autorisation de destinations autorisées (correspondance exacte de domaine, pas startsWith ou endsWith). Résoudre DNS pour chaque domaine autorisé et rejeter si la résolution aboutit à une IP privée/link-local/loopback. Désactiver le suivi des redirections — valider chaque en-tête Location par rapport à la même liste d'autorisation. Journaliser toutes les requêtes sortantes avec destination et statut de réponse pour la détection d'anomalies.