Back to articles
Split-Instruction Exfil: Malicious MCP Servers Steal SSH Keys by Filling In a Form

Split-Instruction Exfil: Malicious MCP Servers Steal SSH Keys by Filling In a Form

A malicious MCP server splits a theft across tool description and results. Agents then send SSH keys and .env as form fields. APRF maps the host gates.

MCP split-instruction exfil is Tool Safety and AI Security first—server-side tool authz, allowlists, high-impact gates on secret-file reads, injection suites that include split cases, human approval, MCP pin/review, and traces. Part of APRF Incident Analysis.

AI Production Readiness Framework

Related APRF controls

Failure classTOOL ABUSE
SeverityHIGH
Relevant APRF controls8

In plain language

Imagine a clerk who will not hand over the office keys if you ask outright—but will fill a blank form if each step arrives as a separate, boring memo. A researcher-built MCP server does that to coding assistants. One tool advertises empty fields `alpha`–`delta`. Another lists files. A third says “put these contents in those fields for a checksum.” No memo says “steal the SSH key.” The assistant stitches them, reads `.ssh/id_rsa` and `.env`, and posts the bytes as form data. Pin MCP servers, treat results as data, and do not let one tool’s output become another tool’s arguments.

The Incident

A developer connects a “security auditor” MCP server, opens a project, and asks for a scan. The assistant reports integrity verification passed. In the server log: SSH private key, source, `customers.csv`, `.env`. On screen: a checksum. In the log: exfiltration.

Here is why the lock never saw a complete theft.

Why existing thinking failed

The comfort story: models refuse “read `.env` and send it out”; scanners inspect descriptions at install; integrity checks catch rug pulls; prompt hardening holds.

That fails when no single surface contains the request. ASSET’s GhostSplice splits one instruction across tool description, tool result, and on some clients sampling. A description scanner sees `integrity_checker(alpha, beta, gamma, delta)`. A traffic filter sees “fill in the fields.” The refusal never fires because the dangerous ask exists only after fragments share a context window. Classic tool poisoning and rug pull are what those scanners were built for. GhostSplice walks around both.

Research proves it

Confirmed in UMKC ASSET’s 11 August 2026 disclosure and github.com/asset-group/ghostsplice. Across eleven API models, average compliance roughly doubled from 42% (one-piece) to 82% (split in two). GPT-4o, Gemini 2.0 Flash, and Llama-3.3-70B went 0% → 100%. A third fragment inside coding clients breaks survivors: Haiku 4.5 stayed at 0% on the API and complied in Cursor. Only Sonnet 4.6 and Opus 4.6 stayed 0/20—ASSET still notes Sonnet shipping proprietary source with a hardcoded key while redacting obvious secrets. GPT-5.4 hit 90% in Cursor and 100% in Codex CLI. Evidence secrets are synthetic.

Confirmed (sampling path): VS Code / Copilot accepts MCP sampling. ASSET quotes `mcpSamplingService.ts`: `systemPrompt` is prepended unwrapped; the UI shows the server name, not the text; session/Always covers later calls. Cursor, Claude Code, and Claude Desktop reject sampling. A blunt steal still refuses; a bland “pre-validated / provide file contents completely” disposition plus a later mapping succeeds—including a run where the model said no and the log still recorded credentials.

Reported: secondary coverage citing ASSET (prefer the primary if a secondary is thin). Confirmed vendor reply: OpenAI treated this as documented third-party MCP injection/exfil risk, not a model CVE. Unknown: in-the-wild registry malware. Speculated: any one reader’s IDE allowlist and HITL prompts.

Engineering takeaway

The model’s caution is not the control. The host must treat MCP descriptions and results as untrusted, refuse result→args copies, pin every server, and gate secret-file reads. That is the APRF Core map below.

Why this matters

Stake: any coding agent that can read the workspace and call third-party MCP can be talked into mailing SSH keys and `.env` as form fields—without one obviously malicious prompt.

Affected, as far as facts allow: ASSET’s isolated API and client tests (Cursor, VS Code/Copilot, Codex CLI)—not a named customer breach. Class: Agents + MCP + Tools. RAG-only stacks are out unless retrieval is an MCP tool that can also read local secrets.

Can a startup ignore it? No if developers connect community MCP to an agent that can read the repo. Maybe if MCP is off. Inventory quarterly: a “temporary” scanner server is how this class arrives.

Timeline

PhaseDate / windowNotesFact class
Prior art2025Tool poisoning / rug pull; description scannersConfirmed (cited by ASSET)
ExperimentsThrough 2026-08API n=30 one-/two-piece; client n≥10 three-pieceConfirmed (ASSET)
Vendor notifyBefore publishDisclosed; OpenAI repliedConfirmed (ASSET)
Disclosure2026-08-11Write-up + PoC `@dfaee36`Confirmed
Journalism2026-08Secondary explainers citing ASSETReported
Patchn/aNo CVE; documented third-party MCP classConfirmed (ASSET)
MitigationNow (defenders)Pin MCP; result-as-data; HITL on secretsConfirmed guidance

Root Cause

Three host properties coincide: no provenance wall in the context window; single-surface scanners and refusals; the deputy already holds the keys (file read). The MCP tool does not need a new capability—only to ask the deputy to complete a form.

Root cause class: cross-channel indirect prompt injection + confused-deputy exfil via MCP arguments—not host RCE and not a weight-level jailbreak.

APRF Lens

Failure class (APRF / threat-map vocabulary): Prompt Injection (indirect) into Tool Abuse / Excessive Agency, ending in Data Exfiltration (AML.T0086). Cousins AML.T0110 / AML.T0104 name tool poisoning; GhostSplice is the split-channel variant. Also AML.T0051.001, AML.T0053. Informative only—not certification. Soft link: /aprf/threats/.

Profile: Core. Lenses: Agents + MCP (CLI run: Core + `aprf-lens-agents`).

Check families in play (APRF 0.11.0; threat-map grounded):

FamilyIDsWhy
Tool safetyTOL-M1, TOL-M2, TOL-M3Server-side authz, fixed allowlist, extra gate on secret reads / outbound args
AI security / injectionSEC-M1, SEC-M3Untrusted MCP text cannot authorize tools; release suites must include split cases
Human in the loopHUM-M1High-impact file read + send-to-MCP needs an out-of-band decision
Supply chainSCI-M2Pin, owner, review every MCP server—registry install is the delivery
ObservabilityOBS-M1Reconstruct description + result + file read + `integrity_checker` args

Good evidence: MCP results cannot become another tool’s arguments; allowlist cannot grow at runtime; deny tests when a scan names `.ssh` / `.env` as checker inputs; HITL shows resolved paths, not `alpha`; pin/owner/review with zero floating tags; traces for ask → scan → file read → outbound tool; a 1-/2-/3-channel eval gate that must not regress.

Standing caveat: APRF assessments can miss evidence. A repository is not the only source of truth. Runtime config, cloud IAM, secrets stores, vendor consoles, CI variables, production prompts, deployed model/tool versions, and human processes often never appear in source control. Absence of a finding is not proof of readiness. Scoring this attacker PoC is not a score of Cursor, VS Code, or your IDE. Threat-map rows are not certification.

APRF mapping confidence: High. Auditor confidence: Medium — PoC, not a production host.

Abuse Path Analysis

Rendering diagram…

- Reg → Svc: Unreviewed MCP from a registry becomes a principal in the session — cut with SCI-M2 (inventory, pin, owner, review) and TOL-M2 (the agent cannot adopt a new “auditor” tool mid-run).

- Desc + Scan + Deep → Model: Three harmless fragments become one instruction because nothing marks source — SEC-M1 (untrusted input cannot authorize privileged effects); SEC-M3 so evals include split/obfuscated cases, not only blunt “steal `.env`.”

- Model → FS: The deputy already has file read — TOL-M1 (platform disposes; scan results do not grant reads) and TOL-M3 / HUM-M1 on secret-class paths.

- Model → Exfil: Result text copied into another tool’s arguments is the exfil primitive — TOL-M1 plus a host rule: tool output is data, never instructions or args (SEC-M1).

- Sys → Model (alternate): Sampling prepends an unseen system prompt — HUM-M1 must show the text, not only the server name; TOL-M3 if auto-approve is on.

Abuse path confidence: High — end-to-end PoC, client logs, and the published compliance table. Speculative only for in-the-wild registry malware and any one vendor’s unpublished scanner.

Standing caveat (brief): APRF assessments can miss evidence; a repository is not the only source of truth. This graph is the research chain, not your production IAM or IDE policy.

Relevant Controls

APRF CheckCategoryWhy it appliesEvidence you’d expect
TOL-M1Tool SafetyScan-result text must not authorize the next tool or its argsGateway deny tests: result→args = 0
TOL-M2Tool SafetyA new “integrity_checker” must not appear because the model askedPer-agent allowlist; runtime expansion rejected
TOL-M3Tool SafetyReading `.env` / SSH and sending contents outbound is high-impactExtra gate; ungated exec impossible in tests
SEC-M1AI SecurityMCP description + result are untrusted; cannot grant file read or exfilServer-side policy; 0 model-text-only grants
SEC-M3AI SecurityOne-piece jailbreak suites miss GhostSpliceRelease gate with 1-/2-/3-channel cases
HUM-M1Human controlOut-of-band yes on resolved secret-file argsPrompt shows path + destination tool, not `alpha`
SCI-M2Supply chainRegistry MCP is the delivery vehiclePin + owner + review; floating tags = 0
OBS-M1ObservabilityStitch is invisible without a linked trace≥95% request→model→tool→outcome on a canary

Patterns from the GhostSplice PoC (not a production victim)

Assessment evidence: `npx @stackrail-io/aprf@0.1.3 audit` against asset-group/ghostsplice @ `dfaee36c94f3cd23ed775ddd72012d00fa486a75`, Core + Agents, offline. This tree is an attacker pattern library, not Cursor, VS Code, Codex, or a customer host.

On that PoC, host gates are Not Demonstrated (TOL-M1, TOL-M2, TOL-M3, SEC-M3). SCI-M2 and OBS-M1 are Partial (README/log signals, not production inventories). HUM-M1 is Not Applicable here—the repo is not an agent host; the Check still applies to the IDE you run. SEC-M1 Partial is collector noise from attack logs, not a deny gate.

Limits: no live IDE; no production MCP inventory; sampling is ASSET’s VS Code code-path claim, not reproduced in this CLI run.

Auditor confidence: Medium — faithful to the disclosed servers; not a vendor harness.

Standing caveat: APRF assessments can miss evidence. A repository is not the only source of truth. Scanning this PoC cannot prove your MCP JSON or approval prompts are safe.

How to Prevent It

Immediate (24–72h)

- Inventory MCP servers on coding agents. Drop anything without an owner and a pin (SCI-M2).

- Treat tool results as data. Do not copy result text into another tool’s arguments unless policy allows that pair (TOL-M1, SEC-M1).

- Extra gate before `.env` / `.ssh` reads and before those bytes leave in MCP args (TOL-M3, HUM-M1). Show resolved paths, not `alpha`.

- VS Code sampling: disable, or approve the prompt text; turn off Always / global auto-approve.

30 days

- Per-agent allowlists that cannot grow at runtime (TOL-M2). Segment filesystem tools from MCP tools that accept arbitrary strings.

- SEC-M3: add 1-/2-/3-channel and sampling-disposition cases. A suite that only has “steal `.env`” will green-wash this class.

- OBS-M1: canary file; alert if its contents appear in an MCP argument after a “scan” turn.

Longer term

- Procurement: isolated MCP channels? Refuse result→args? Sampling as a system message without showing the text?

- Assume description scanners stay one-surface. Invest in which tools may follow which, not another keyword list.

Engineering Lessons

1. Refusal is a tripwire, not a boundary—if the request never arrives in one piece, the tripwire never fires.

2. Description scanners lose when the payload is in the result—and result scanners lose when the schema was in the description.

3. Tool output must not become tool arguments without an explicit, host-side allow for that pair.

4. The client is a control plane—same model, different leak rate (Cursor vs Claude Code vs Codex vs VS Code sampling).

5. Pinning MCP servers is load-bearing—GhostSplice is a malicious server you chose to trust, not a model CVE.

Could this happen to you?

You should read this if…

- ✓ You have agents

- ✓ You use MCP

- ✓ You expose tools to models

- ✓ Those tools can read the workspace or secrets

- ✓ You install community / registry MCP servers

- ✓ You do not gate secret-file reads or outbound MCP arguments

- ✓ You pin neither MCP servers nor plugin versions

If two or more apply, treat the Relevant Controls table as a gate checklist this week.

Continues in the APRF Incident Corpus

- Related: MCP tool allowlists / confused deputy — unbounded catalogs; here a small catalog still loses if results instruct the next call.

- Related: Encrypted prompt injection / Grok session exfil — another one-surface scanner miss; delivery is web browse, not MCP.

Soft CTA: self-attest against Core (questionnaire—not certification) at /aprf/assess/. For evidence-backed runs against your agent host, use the open APRF Auditor skill.

FAQ

Would assessing the public GhostSplice repo have caught a real victim? That repo is the attacker. Useful as a pattern library. Assess your agent host and MCP inventory.

Same as the confused-deputy / tool-allowlist class? Related, not the same. That piece is open catalogs. This is split instructions so scanners and refusals miss a small, approved-looking set. A tight allowlist still loses if `integrity_checker` is allowed and results can fill its args.

Same as the Grok encrypted-instruction analysis? Same family of one-surface scanner miss; different channel (browse+decrypt vs MCP description+result). Both need provenance and argument gates.

What can APRF miss outside a repo? Live MCP JSON, HITL text, sampling on/off, model routing, whether results are labeled untrusted. This run did not probe a running IDE.

Is this tool poisoning? Confirmed (ASSET’s definition): no complete malicious instruction in a description, no post-approval behavior swap. Scanners built only for those two patterns are out of position.

References

1. ASSET Research Group — The AI refused to steal the secrets. So we handed it a form. (2026-08-11)

2. ASSET — github.com/asset-group/ghostsplice @ `dfaee36c94f3cd23ed775ddd72012d00fa486a75`

3. APRF 0.11.0 Check catalog + threat map — /aprf/spec/, /aprf/threats/

4. Related corpus — MCP tool allowlists / confused deputy, Grok encrypted prompt injection

Need hands-on help? Book a free 30-minute production audit.

Book Free 30-Min Production Audit

View our DevSecOps services

Next: Tool Safety

Open the related pillar specification for mandatory checks, artifacts, and pass conditions. Self-attest is optional.