The archive/CVE-2025-64340
PrefectHQ/fastmcpDisclosed 2025

CVE-2025-64340:fastmcp: OS command injection via server name in subprocess-backed in

A historical CVE, retraced through the questions Lachesis asks of vulnerable code.

Historical recordv3.1.0 (affected: < 3.2.0)
WeaknessCWE-78 · OS command injection via server name in subprocess-backed in
ImpactA os command injection via server name in subprocess-backed in in fastmcp, reachable from attacker-controlled input.
Read the reconstruction
01What happened

On Windows, FastMCP let a server name reach a command wrapper that treated shell characters as instructions.

That is the shape of the failure. The rest of this case file follows the evidence behind it.

Separate the record from the run

What was known.
What Lachesis established.

01Historical record

Known before Lachesis ran

The CVE and vulnerable release were selected from public history. We are not claiming novel discovery.

CVE
CVE-2025-64340
Vulnerable target
v3.1.0 (affected: < 3.2.0) · vulnerable
Prior knowledge
CVE-2025-64340 · v3.1.0 (affected: < 3.2.0) is vulnerable
then
02Lachesis run
Verified captured run

Independent re-detection

The experiment graph was built around the vulnerable install modules (claude_code.py, gemini_cli.py) and their shared.py (release v3.1.0, affected < 3.2.0), rather than starting from arbitrary FastMCP source. We did not point the tool at the CVE. Enumeration ran over the whole candidate registry (every family); the two install subprocess.run sinks surfaced on their own alongside filesystem and navigation leads, and Lachesis carried the untrusted install arguments into the shell call. The sast_output fields are the enumerator's own emission; the Windows-specific exploit mechanism under adjudication is my reading of the advisory and fix.

Seeded inputs
None
Run timestamp
2026-08-24T12:06:09Z
Evidence artifact
~/.lachesis/graphs/fastmcp_install.kuzu

The commands and outputs below come from this recorded Lachesis run.

The reconstruction, in one frame

What Lachesis reconstructed.

One historical repository. Four captured queries. A complete source-to-sink argument.

Lachesis Casefiles · Unbound ComputeVerified runCVE-2025-64340
A server name reached Windows command parsing as if it were trusted input.
01Sourcedef install_claude_code(..., name: str, ...):
02Sinksubprocess.run(cmd_parts, check=True, capture_output=True, text=True)
03Missing guardcmd_parts = [claude_cmd, "mcp", "add", name]
$ lachesis sources_of → reaches → guard_dominance
PrefectHQ/fastmcp · v3.1.0 (affected: < 3.2.0) · vulnerableunboundcompute.com
This was known history. Your code is not.

Lachesis follows the same evidence path through unfamiliar repositories.

The Lachesis reconstruction

Here is how the path becomes visible.

These are the recorded questions Lachesis asked of the historical vulnerable code, followed by the raw result and source location each query returned.

RepositoryPrefectHQ/fastmcp
Revisionv3.1.0 (affected: < 3.2.0) · vulnerable
EngineLachesis

Captured runThis is a real replay of Lachesis over the graph we built for this case. Every command below was run against the fastmcp install graph and every result is the output captured on that drive. We did not name a family. The hunt listed the whole taxonomy first and the subprocess command sink came out of it alongside other leads. No server name string was seeded; the graph was built from the source alone and the finding was rediscovered from graph structure, so the mode is independent-redetection.

01 · load_graph01 / 05
Lachesis ran
load_graph --path ~/.lachesis/graphs/fastmcp_install.kuzu --profile all && candidate_census

Load the graph and list every bug family

First we load the graph and ask the tool to list every bug family, with nothing chosen ahead of time. It reports all 8 domains and 31 sink constructors and says the census is complete for what the graph can observe. Injection is one row among all of them, not the target we aimed at.

Captured resultCaptured · load_graph
raw captured outputload_graph
[lachesis-mcp] loaded the graph; overlay: 0 derived edges; dataflow tier: on demand, per cone

CANDIDATE_CENSUS
move: candidate_census
taxonomy (8):
  domain=lifecycle         title=Resource lifecycle              enumerable=True
  domain=memory            title=Memory safety                   enumerable=True
  domain=injection         title=Injection                       enumerable=True
  domain=navigation        title=Request forgery & redirection   enumerable=True
  domain=object-integrity  title=Object integrity                enumerable=True
  domain=filesystem        title=Filesystem                      enumerable=True
  domain=crypto-config     title=Cryptography & transport config enumerable=True
  domain=resource          title=Resource exhaustion             enumerable=True
constructors (31): complete_for_observable_graph=True
applied: True
role_nodes: {sink: 39, source: 3}
src/fastmcp/cli/install/claude_code.pyline 0
Code location returned for this evidence step.
The repair

A small line with a large consequence.

The server name is not checked for shell metacharacters before subprocess.run executes it. On Windows, the .cmd wrapper can pass those characters to cmd.exe.

Before
cmd_parts = [claude_cmd, "mcp", "add", name]
After the patch
// validate server names / reject shell metacharacters before building the command (v3.2.0, PR #3522).
Sources & further reading

The original record.

Read the historical advisory and vulnerability record behind this reconstruction.

Open the Lachesis quickstart