Donner aux utilisateurs la possibilité de configurer des webhooks (URL de callback appelées lors d'événements) est une fonctionnalité standard pour toute plateforme SaaS. Cependant, si votre code se contente d'exécuter un POST sur l'URL fournie sans validation approfondie, vous venez d'ouvrir une brèche majeure : une faille SSRF (Server-Side Request Forgery). En voici l'analyse technique.
La vulnérabilité : comment l'attaquant exploite le serveur
Puisque l'appel HTTP part de votre propre serveur (qui réside à l'intérieur de votre infrastructure cloud ou de votre réseau privé), l'attaquant peut configurer un webhook pointant vers des adresses IP locales non exposées à internet.
• URL d'accès aux métadonnées AWS : http://169.254.169.254/latest/meta-data/ (Expose les clés d'accès IAM du serveur !)
• URL d'un serveur Redis interne : http://192.168.1.50:6379/
Le serveur web exécute la requête, traverse les pare-feux externes (car le trafic est sortant/outbound), interroge le service interne, et renvoie parfois la réponse (ou le statut) à l'attaquant, dévoilant l'architecture interne.
La mauvaise parade : les listes noires de chaînes de caractères
Tenter de bloquer les chaînes comme "localhost" ou "127.0.0.1" est inefficace. Un attaquant peut contourner cela en utilisant des représentations alternatives (ex: décimale http://2130706433), en configurant un sous-domaine DNS personnel qui pointe vers 127.0.0.1 (ex: spoof.attaquant.com), ou en exploitant une redirection HTTP 302 émise par son propre serveur.
La solution robuste : Résolution DNS et filtrage d'IP (RFC 1918)
La seule méthode fiable consiste à résoudre le nom de domaine en adresse IP applicativement avant d'effectuer la requête HTTP, puis à valider que cette IP n'appartient pas aux plages d'adresses privées (RFC 1918) ou réservées.
Voici l'algorithme de validation en Node.js / TypeScript :
import dns from "dns/promises";
import ipaddr from "ipaddr.js";
async function isSafeUrl(inputUrl: string): Promise<boolean> {
try {
const parsed = new URL(inputUrl);
// 1. Forcer la résolution DNS pour obtenir les IPs sous-jacentes
const addresses = await dns.resolve(parsed.hostname);
for (const addr of addresses) {
const ip = ipaddr.parse(addr);
// 2. Vérifier si l'IP appartient à une plage privée ou réservée
const range = ip.range();
if (
range === "private" || // Plages 10.x.x.x, 172.16.x.x, 192.168.x.x
range === "loopback" || // 127.0.0.1
range === "linklocal" || // 169.254.x.x (AWS Metadata)
range === "unspecified"
) {
// IP non sécurisée, rejet immédiat
return false;
}
}
return true; // L'IP résolue est publique et sécurisée
} catch (err) {
return false; // Rejet en cas d'erreur de résolution
}
}Règles d'or additionnelles pour les webhooks
- Désactiver le suivi des redirections : Forcez le client HTTP (ex: Axios, Axios instance) à ne pas suivre automatiquement les redirections HTTP (
maxRedirects: 0) pour éviter les attaques par rebond de redirection 302. - Réseau isolé (DMZ) : Exécutez le worker chargé d'envoyer les webhooks dans un sous-réseau (VPC) isolé, sans aucun accès aux autres serveurs de base de données ou de cache de l'entreprise.
- Timeout strict : Fixez des limites temporelles courtes (ex: 2 secondes de connexion max) pour éviter le blocage des threads de votre serveur par des connexions dormantes (Slowloris).
