Retour au cours

cyber / cybersecurite-fondamentale

OWASP Top 10 : Server-Side Request Forgery (SSRF)

Leçon 111 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi une fonctionnalité de requête sortante (avatar, prévisualisation de lien) peut devenir une porte d'entrée
  • Identifier les cibles typiques d'un SSRF (métadonnées cloud, services internes, fichiers locaux)
  • Expliquer pourquoi une liste noire d'adresses ne suffit jamais à s'en protéger
  • Mettre en place une liste blanche de destinations avec vérification de l'IP réellement résolue
  • Comprendre le piège du DNS rebinding et pourquoi la vérification doit se faire juste avant la connexion

Dans quel contexte ?

Un site permet à ses utilisateurs d'importer un avatar depuis une URL externe. Un attaquant fournit l'URL http://169.254.169.254/latest/meta-data/iam/security-credentials/, qui correspond au service de métadonnées d'une instance cloud AWS. Si le serveur ne vérifie pas la destination, il va lui-même chercher et potentiellement afficher des identifiants d'accès internes au cloud de l'entreprise, sans qu'aucun pare-feu externe n'ait pu l'empêcher : la requête part de l'intérieur.

Le serveur comme complice involontaire

Beaucoup d'applications ont une fonctionnalité légitime qui fait une requête réseau sortante à partir d'une URL fournie par l'utilisateur : télécharger un avatar, prévisualiser un lien, importer un fichier distant. Le SSRF (Server-Side Request Forgery) détourne cette fonctionnalité pour faire faire au SERVEUR une requête vers un endroit qu'il ne devrait jamais atteindre — typiquement des ressources internes invisibles depuis l'extérieur (bases de données internes, panneaux d'administration, ou pire, les métadonnées cloud qui peuvent contenir des identifiants d'accès).

Pourquoi c'est particulièrement dangereux

Contrairement à un attaquant externe bloqué par le pare-feu, le serveur, lui, est DÉJÀ à l'intérieur du réseau. En le forçant à faire une requête à sa place, l'attaquant emprunte sa position de confiance pour atteindre des ressources normalement hors de portée. C'est un peu comme convaincre un employé de confiance de porter un message à l'intérieur d'un bâtiment sécurisé, plutôt que d'essayer d'y entrer soi-même.

Pourquoi une liste noire ne fonctionne jamais

On pourrait penser qu'il suffit d'interdire les adresses privées connues (127.0.0.1, 169.254.169.254). Mais un attaquant dispose de nombreuses façons d'écrire la même adresse pour contourner un simple filtre textuel.

Cible viséeExemple d'URL piégéeCe qu'elle expose
Métadonnées cloudhttp://169.254.169.254/latest/meta-data/Identifiants d'accès IAM
Service internehttp://localhost:6379/Base Redis non exposée publiquement
Fichier localfile:///etc/passwdLecture de fichiers du serveur
Panneau d'admin internehttp://internal-admin.local:8080/Fonctions d'administration non protégées en externe

Piège fréquent

La seule défense fiable est une liste BLANCHE de destinations autorisées, associée à une vérification de l'adresse IP réellement résolue au moment de la connexion — jamais une liste noire, toujours contournable (notation décimale, hexadécimale, IPv6, astuces d'URL).

Le piège du DNS rebinding

Un domaine peut légitimement pointer vers une IP publique au moment du contrôle de sécurité, puis être reconfiguré pour pointer vers une IP interne juste avant que la requête réelle ne parte. C'est pourquoi la vérification DNS doit se faire juste avant la connexion, pas en amont dans le traitement.

À retenir

Bonne pratique

La défense applicative (liste blanche) et la défense réseau (pare-feu sortant) se complètent : ne jamais compter sur une seule couche.

Commandes & code

OWASP Top 10 : Server-Side Request Forgery (SSRF)

Le SSRF survient quand une application effectue une requête HTTP vers une URL contrôlée (ou influencée) par l'utilisateur, permettant d'atteindre des ressources internes normalement inaccessibles depuis l'extérieur.

python
# VULNÉRABLE : l'application récupère une image depuis une URL fournie par l'utilisateur
import httpx
from fastapi import FastAPI

app = FastAPI()

@app.post("/import-avatar")
async def import_avatar_vulnerable(image_url: str):
    async with httpx.AsyncClient() as client:
        response = await client.get(image_url)  # aucune restriction sur la destination
    return {"size": len(response.content)}

# Payloads de démonstration en lab autorisé, exploitant la confiance du serveur :
# image_url = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"  # métadonnées cloud AWS
# image_url = "http://localhost:6379/"                                             # Redis interne
# image_url = "http://internal-admin-panel.local:8080/delete-all"                  # service interne non exposé
# image_url = "file:///etc/passwd"                                                  # lecture de fichier local
python
# CORRECT : liste blanche stricte de domaines/IP autorisés + résolution DNS vérifiée
import ipaddress
import socket
from urllib.parse import urlparse

ALLOWED_HOSTS = {"images.trusted-cdn.com", "assets.technologik.local"}

def is_private_ip(ip: str) -> bool:
    addr = ipaddress.ip_address(ip)
    return addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved

def validate_outbound_url(url: str) -> str:
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        raise ValueError("schéma non autorisé")
    if parsed.hostname not in ALLOWED_HOSTS:
        raise ValueError("hôte non autorisé (liste blanche)")

    # Résoudre le DNS et vérifier que l'IP réelle n'est pas privée (contre le DNS rebinding)
    resolved_ip = socket.gethostbyname(parsed.hostname)
    if is_private_ip(resolved_ip):
        raise ValueError("résolution vers une IP privée refusée")
    return url

@app.post("/import-avatar")
async def import_avatar_safe(image_url: str):
    validated = validate_outbound_url(image_url)
    async with httpx.AsyncClient(follow_redirects=False, timeout=5.0) as client:  # pas de redirects auto
        response = await client.get(validated)
    return {"size": len(response.content)}
text
# Vecteurs de contournement de filtre à connaître (pour bien défendre)
1. Redirection HTTP : URL initiale autorisée qui redirige (302) vers une IP interne -> désactiver follow_redirects
2. DNS rebinding : le domaine autorisé résout vers une IP publique au moment du check, puis interne au moment du fetch
3. Encodage alternatif d'IP : http://0x7f000001/ ou http://2130706433/ (équivalents de 127.0.0.1)
4. IPv6 : http://[::1]/ équivalent loopback en IPv6
5. URLs avec userinfo trompeur : http://trusted.com@169.254.169.254/ (le host réel est après le @)
python
# Défense supplémentaire : réseau (pas seulement applicatif) - bloquer l'accès aux métadonnées cloud
# via une règle de pare-feu sortant, indépendamment du code applicatif
# Exemple iptables sur l'hôte du service qui fait les requêtes sortantes
"""
iptables -A OUTPUT -d 169.254.169.254 -j DROP
"""
text
# Checklist défense SSRF
- Liste blanche de destinations (jamais liste noire, toujours contournable)
- Résolution DNS vérifiée juste avant la connexion (éviter le TOCTOU / DNS rebinding)
- Désactiver les redirections HTTP automatiques ou les revalider à chaque saut
- Timeout court + limite de taille de réponse (éviter l'exfiltration/déni de service)
- Isoler le service qui fait des requêtes sortantes dans un réseau sans accès aux ressources internes sensibles

Résumé

  • Le SSRF détourne le serveur pour atteindre des ressources internes (métadonnées cloud, services locaux).
  • La liste blanche stricte de destinations, vérifiée après résolution DNS, est la défense de base.
  • Désactiver les redirections automatiques et se prémunir contre le DNS rebinding sont essentiels.
  • Une défense réseau (firewall sortant) complète la défense applicative.

Exercices pratiques

1 disponible
1

Mission : l'aperçu de lien qui interroge Redis

Objectif : Repérer une faille SSRF dans une fonctionnalité légitime et concevoir une défense qui résiste au DNS rebinding.

Contexte

La fonctionnalité "aperçu de lien" du forum Technologik récupère et affiche un extrait de la page dont l'URL est collée par l'utilisateur, via une requête HTTP faite par le serveur lui-même. Un utilisateur signale un comportement étrange : coller l'URL http://localhost:6379/ fait planter l'aperçu avec une réponse binaire illisible au lieu d'une erreur de connexion propre.

Résoudre l’exercice →