Server Implementation
A tool calls `pickle.loads`, `yaml.load`, `marshal.loads`, or an equivalent on bytes it did not itself produce, letting the payload construct arbitrary objects and execute arbitrary code.
Some serialization formats (pickle, Java serialization, unsafe YAML) are not just data formats — they are tiny programs that the deserializer executes while rebuilding the object graph. Calling `pickle.loads(untrusted_bytes)` is equivalent to calling `exec(untrusted_bytes)` with extra steps. The common shapes in MCP are a cache tool that `pickle.load`s a file the model specified, or a YAML-config tool that uses the default (unsafe) loader.
MCP servers often accept file paths or blob references from the model and round-trip them through a cache or queue. Each round-trip is a chance to slip in an attacker-controlled payload. Because deserialization runs before any tool-level validation, a single poisoned cache entry becomes remote code execution inside the server process with no further interaction required.
@server.tool() |
def load_session(path: str) -> dict: |
# pickle.loads on caller-supplied bytes — any .pkl payload = RCE |
return pickle.loads(Path(path).read_bytes()) |
@server.tool() |
def load_session(path: str) -> dict: |
# JSON cannot express arbitrary Python objects, so it cannot execute code. |
return json.loads(Path(path).read_text()) |
# For YAML, use safe_load; never yaml.load(...) with the default loader. |
def load_config(path: str) -> dict: |
return yaml.safe_load(Path(path).read_text()) |
We flag `pickle.load(s)`, `cPickle.load(s)`, `marshal.load(s)`, `yaml.load` without a `Loader=SafeLoader` kwarg, `jsonpickle.decode`, and Node.js `node-serialize.unserialize`. We do not flag `json.loads` or `yaml.safe_load`.
See the full threat catalog for every documented detection.
CVEs of the same CWE class. Not MCP-specific, but exemplify the failure mode MCPSafe detects.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now