Topic: mcp server object injection security
MCP server object injection security — unsafe deserialization and eval patterns
Object injection covers a class of vulnerabilities where attacker-controlled data is interpreted as executable code or as a live object with callable methods. In an MCP server, the attacker-controlled data is the AI model's tool argument values. If a tool handler passes those values to eval(), the Function() constructor, node-serialize, Python's pickle, or similar mechanisms that bridge data and code, the result is remote code execution in the server process — with whatever system access the server process holds.
The three most common object injection patterns in MCP servers
Pattern 1: eval() on tool arguments
// VULNERABLE: eval() on AI-supplied expression argument
server.tool('calculate', z.object({
expression: z.string(),
}), async ({ expression }) => {
// If expression = "require('child_process').execSync('cat /etc/passwd')"
// this executes arbitrary code in the server process
const result = eval(expression) // CRITICAL: eval of AI-supplied string
return { result: String(result) }
})
// ALSO VULNERABLE: new Function() constructor
const fn = new Function('x', `return ${expression}`) // equivalent to eval
// SAFE ALTERNATIVE: use a math parser library for expression evaluation
import { evaluate } from 'mathjs'
server.tool('calculate', z.object({
expression: z.string().max(500),
}), async ({ expression }) => {
try {
const result = evaluate(expression) // math-only, no code execution
if (typeof result !== 'number') throw new Error('Non-numeric result')
return { result }
} catch (e) {
return { error: 'Invalid expression' }
}
})
Pattern 2: node-serialize IIFE exploitation
// VULNERABLE: node-serialize.unserialize() on AI-supplied data
import serialize from 'node-serialize'
server.tool('loadConfig', z.object({
serializedConfig: z.string(),
}), async ({ serializedConfig }) => {
// Malicious payload: {"rce":"_$$ND_FUNC$$_function(){require('child_process').execSync('id')}()"}
// node-serialize evaluates _$$ND_FUNC$$_ strings as JavaScript functions
// and if they are IIFEs (ending in ()), executes them immediately on unserialize
const config = serialize.unserialize(serializedConfig) // CRITICAL: RCE
return applyConfig(config)
})
// SAFE ALTERNATIVE: use JSON.parse() for data interchange
server.tool('loadConfig', z.object({
config: configSchema, // validated Zod schema, not raw serialized string
}), async ({ config }) => {
return applyConfig(config)
})
// If you must accept a serialized string, use JSON.parse with schema validation:
server.tool('loadConfig', z.object({
configJson: z.string().max(10000),
}), async ({ configJson }) => {
let parsed: unknown
try {
parsed = JSON.parse(configJson)
} catch {
return { error: 'Invalid JSON' }
}
const result = configSchema.safeParse(parsed)
if (!result.success) return { error: 'Invalid config structure' }
return applyConfig(result.data)
})
Pattern 3: Python pickle deserialization
# VULNERABLE: pickle.loads() on AI-supplied base64 data
import pickle, base64
@server.tool()
async def load_model(model_data: str) -> dict:
# If model_data contains a pickle payload that sets __reduce__,
# pickle.loads() will execute arbitrary Python code
data = base64.b64decode(model_data)
model = pickle.loads(data) # CRITICAL: arbitrary code execution
return {"loaded": True}
# SAFE ALTERNATIVE: use JSON or MessagePack for data exchange
import json
from pydantic import BaseModel
class ModelConfig(BaseModel):
weights_url: str
architecture: str
version: str
@server.tool()
async def load_model(config_json: str) -> dict:
try:
raw = json.loads(config_json)
config = ModelConfig(**raw)
except (json.JSONDecodeError, ValueError) as e:
return {"error": "Invalid config"}
# Load model from validated URL, not from deserialized binary
model = await fetch_model_from_url(config.weights_url)
return {"loaded": True, "version": config.version}
What SkillAudit checks
eval()calls where the argument is derived from a tool argument — CRITICAL; direct code injection path; any tool argument that reacheseval()is exploitablenew Function()orFunction()constructor with AI-supplied string arguments — CRITICAL; equivalent toeval(); same severitynode-serialize.unserialize()on tool arguments — CRITICAL; well-documented IIFE RCE vector; zero legitimate use case for this pattern- Python
pickle.loads(),cPickle.loads(), ormarshal.loads()on AI-supplied data — CRITICAL; pickle deserialization is arbitrary code execution - Node.js
vm.runInContext()orvm.runInNewContext()with AI-supplied code strings — HIGH; Node.js VM is not a security sandbox — breakout viathis.constructor.constructoris well-documented; full sandbox escape should be assumed YAML.load()(PyYAML unsafe loader) on AI-supplied YAML strings — HIGH; PyYAML's full loader supports Python object construction via!!python/object/apply:tags; useyaml.safe_load()instead
See also
- MCP server deserialization security — broader coverage of unsafe deserialization patterns beyond object injection
- MCP server command injection — related class using shell execution rather than language eval
- MCP server input validation — schema-first argument design that prevents unvalidated strings from reaching dangerous sinks
- MCP server security checklist — pre-publication hardening checklist
Check your MCP server for eval, object injection, and unsafe deserialization patterns.
Run a free audit → How grading works →