← Back to Blog
CVE-2026-39885 · CVSS 7.5 HIGH

Imagine this. You're a developer building AI-powered tools. You discover mcp-from-openapi, a neat npm package that takes any OpenAPI spec and spins up an MCP server automatically. You feed it your API definition, and suddenly your AI agent can call your endpoints. Magic.

Now imagine someone sends you an OpenAPI spec. Maybe it's a partner integration. Maybe it's a public API you found online. You load it into the tool. Everything looks normal. But buried inside that YAML file is a single line that just emptied your cloud credentials onto an attacker's server.

This is the story of CVE-2026-39885 — a vulnerability I reported through GHSA-v6ph-xcq9-qxxj.

The Setup: What Is FrontMCP?

FrontMCP is an open-source toolkit that bridges OpenAPI specifications and the Model Context Protocol (MCP). At its core, the mcp-from-openapi package takes any Swagger/OpenAPI definition and generates an MCP server that exposes those API endpoints as tools for AI agents to call. The @frontmcp/adapters and @frontmcp/sdk packages build on top of it.

If you're building AI agents that interact with APIs, this is exactly the kind of tool you'd reach for. It saves you from writing boilerplate MCP tool definitions by hand.

The problem wasn't in the MCP layer. It was in how the package parsed OpenAPI specs.

The Vulnerability: A Trusted Parser, Unconfigured

OpenAPI specs support something called $ref — JSON references that point to other parts of the document, or to external files. This is a standard feature. It keeps large API definitions clean and modular.

Under the hood, mcp-from-openapi uses a library called @apidevtools/json-schema-ref-parser to resolve these references. This library is powerful. It can follow $ref pointers to local files, HTTP URLs, even internal network addresses.

Here's the catch: the library was called with zero restrictions.

// The vulnerable pattern (simplified)
const $RefParser = require('@apidevtools/json-schema-ref-parser');

// No URL allowlist. No protocol restrictions. No custom resolvers.
const api = await $RefParser.dereference(openapiSpec);

No options passed. No URL allowlist. No protocol filtering. The ref parser was handed the keys to the network and told "fetch whatever you want."

The Attack: Three Lines That Steal Your Identity

An attacker crafts a malicious OpenAPI spec. It looks perfectly normal — paths, schemas, responses, all standard stuff. Except one schema has a $ref that doesn't point to another part of the document.

Stealing AWS Credentials

If the package runs on an EC2 instance, ECS task, or Lambda function, the cloud metadata service is reachable at a well-known IP address. The attacker adds a reference that points straight to it:

components:
  schemas:
    UserProfile:
      $ref: "http://169.254.169.254/latest/meta-data/iam/security-credentials/"

When the ref parser encounters this, it dutifully makes an HTTP request to the AWS Instance Metadata Service (IMDS). The response? Temporary IAM credentials — Access Key, Secret Key, Session Token. Everything an attacker needs to assume that role.

The parser doesn't know it just fetched cloud credentials. It tries to interpret the response as a JSON schema, probably fails silently or throws a parse error — but the HTTP request already happened. If the attacker points to a server they control as a middle hop, the data is exfiltrated.

Why this matters: IMDSv1 responds to any HTTP GET from the instance. No special headers required. One $ref, one HTTP call, and the attacker has your IAM role's temporary credentials. Even with IMDSv2, if the token was already fetched by a process on the same instance, the metadata service is still reachable.

Reading Local Files

It's not just network calls. The ref parser also supports the file:// protocol:

components:
  schemas:
    Config:
      $ref: "file:///etc/passwd"
    PrivateKey:
      $ref: "file:///home/app/.ssh/id_rsa"
    EnvSecrets:
      $ref: "file:///proc/self/environ"

SSH keys, environment variables (which often hold API tokens and database passwords), configuration files — all readable. The parser fetches the file contents and tries to use them as a JSON schema. The data passes through the application.

Scanning Internal Networks

The third attack vector is reconnaissance. An attacker probes internal services by referencing internal IPs and ports:

components:
  schemas:
    Probe1:
      $ref: "http://10.0.0.1:6379/"
    Probe2:
      $ref: "http://10.0.0.1:5432/"
    Probe3:
      $ref: "http://10.0.0.1:9200/"

Connection refused? That port is closed. Timeout? Probably firewalled. Got a response? There's a Redis, Postgres, or Elasticsearch instance at that address. The attacker just mapped your internal network topology without ever touching it directly.

The Blast Radius

This wasn't just one package. The vulnerability affected three npm packages:

And here's the uncomfortable part: no authentication is required to exploit this. The attacker doesn't need access to your server. They just need you to load their OpenAPI spec. That's it. The attack vector is the spec itself.

Think about how OpenAPI specs travel: shared in Slack, downloaded from public repos, fetched from URLs during CI/CD, pulled from API marketplaces. The supply chain surface is massive.

CVSS 7.5 (High) — Network attack vector, low complexity, no privileges required, no user interaction needed, high confidentiality impact. This scores high because the barrier to exploitation is essentially zero.

The Fix

The remediation is straightforward. The ref parser supports resolver options that were simply never configured:

const api = await $RefParser.dereference(openapiSpec, {
  resolve: {
    file: false,          // Block file:// entirely
    http: {
      headers: {},
      timeout: 5000,
      // Only allow refs to explicitly trusted hosts
      canRead: (file) => {
        const url = new URL(file.url);
        return allowedHosts.includes(url.hostname);
      }
    }
  }
});

Disable file:// protocol. Restrict HTTP resolution to an explicit allowlist of trusted hosts. Or, if you don't need external refs at all, disable all external resolution entirely.

The patched versions:

The Bigger Lesson

This CVE is a textbook example of something I see constantly in security: a powerful library used with its default (permissive) configuration.

The ref parser isn't broken. It did exactly what it was designed to do — resolve references from any source. The vulnerability was in the consumer code that called it without thinking about what "any source" really means when you're processing untrusted input.

This pattern shows up everywhere:

The story is always the same: a developer finds a library that solves their problem, calls the happy-path function, and ships it. The library's security-relevant configuration options — the ones buried three pages deep in the docs — never get set.

For Developers Building MCP Tools

The MCP ecosystem is exploding right now. New tools are being published daily. If you're building MCP servers that accept external input — OpenAPI specs, JSON schemas, configuration files — treat that input as hostile.

Final Thought

The scariest vulnerabilities aren't the ones that require a complex exploit chain. They're the ones where the attacker just hands you a file, and you do all the work for them.

A single $ref. A single HTTP request you never intended to make. And suddenly, your cloud credentials are someone else's.

Patch your dependencies. Audit your parsers. And never trust a spec you didn't write.

Full advisory: GHSA-v6ph-xcq9-qxxj | Patch: FrontMCP v1.0.4