MCP Server Security
Deserialization security for MCP servers
MCP servers parse untrusted JSON from tool arguments, configuration files, and downstream API responses. JSON itself is safe from code execution — but the patterns around it are not. Prototype pollution via __proto__, unsafe YAML parsers that execute tags, and reviver functions that trust structure are real attack surfaces found in community MCP servers.
The prototype pollution attack
Native JSON.parse does not execute code, making it safe from the RCE bugs in Java or Python pickle. But it does construct JavaScript objects, and those objects can carry properties that poison your application's prototype chain when merged naively.
An attacker controlling a JSON string includes {"__proto__": {"isAdmin": true}}. When JSON.parse processes this and the result is merged into any object via a naive deep-merge function, every object in the process inherits isAdmin: true from Object.prototype.
// Vulnerable deep merge — common in config-loading code
function merge(target, source) {
for (const key of Object.keys(source)) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {}
merge(target[key], source[key]) // __proto__ merges into prototype
} else {
target[key] = source[key]
}
}
}
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}')
merge({}, malicious)
console.log({}.isAdmin) // true — Object.prototype poisoned
Prototype pollution mitigations
// 1. Use Object.create(null) — no prototype to pollute
const parsed = Object.assign(Object.create(null), JSON.parse(input))
// 2. Sanitise keys before any deep merge
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = safeMerge(target[key] ?? {}, source[key])
} else {
target[key] = source[key]
}
}
return target
}
// 3. JSON.parse with a reviver that blocks dangerous keys
const safe = JSON.parse(input, (key, value) => {
if (key === '__proto__' || key === 'constructor') return undefined
return value
})
Reviver function hardening
JSON.parse's optional reviver becomes dangerous when it trusts structure — calling constructors on parsed type tags or using eval-adjacent patterns to restore class instances.
// Dangerous: reviver that instantiates based on type field
const unsafe = JSON.parse(input, (key, value) => {
if (value && value.__type === 'RegExp') {
return new RegExp(value.source, value.flags) // attacker controls source
}
return value
})
// Safe: whitelist only the exact transformations you need
const allowedTypes = new Set(['ISODate'])
const safe = JSON.parse(input, (key, value) => {
if (value?.__type === 'ISODate' && typeof value.value === 'string') {
return new Date(value.value)
}
return value
})
Unsafe YAML parsers
YAML's !! type tags can instantiate arbitrary JavaScript objects. In js-yaml 3.x, !!js/function and !!js/eval execute code on parse. Even without these tags, YAML's alias and anchor system creates DoS risk via "billion laughs" exponential expansion.
// js-yaml 3.x — dangerous default (executes !!js/eval tags)
import yaml from 'js-yaml'
const result = yaml.load(userInput) // RCE if input contains !!js/function
// Safe: FAILSAFE_SCHEMA or JSON_SCHEMA
const safe = yaml.load(userInput, { schema: yaml.FAILSAFE_SCHEMA })
// Best: upgrade to js-yaml 4.x — removed !!js/* tags entirely
// Add a size limit to prevent billion-laughs DoS
const MAX = 1024 * 1024 // 1 MB
if (Buffer.byteLength(raw) > MAX) throw new Error('Config too large')
const config = yaml.load(raw, { schema: yaml.JSON_SCHEMA })
Schema-validate everything after parse
Regardless of format, validate parsed data with Zod or another schema library before using it. Parser output structure is not a guarantee of semantic validity.
import { z } from 'zod'
const ConfigSchema = z.object({
port: z.number().int().min(1).max(65535),
allowed_dirs: z.array(z.string()),
api_key: z.string().optional()
})
const config = ConfigSchema.parse(raw) // throws on unexpected structure
What SkillAudit flags
- js-yaml 3.x
yaml.load()without schema restriction — High (RCE via !!js/eval) - Deep-merge of JSON.parse output without key sanitisation — High (prototype pollution)
- Reviver that calls constructors on parsed type tags — High
- YAML parse without size limit — Medium (DoS risk)
- No schema validation after parse — Low (logic errors from malformed input)
Scan your MCP server for deserialization risks
SkillAudit checks for js-yaml unsafe usage, prototype pollution patterns, and reviver anti-patterns. Free graded report in 60 seconds.
Run a free audit →