c launch(config: SandboxConfig): Promise<string> {
const executionId = createHash('sha256').update(config.toolId).digest('hex').slice(0, 12);
// Enforce least-privilege execution environment
const proc = spawn('node', ['--no-warnings', 'server.js'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, MCP_TOOL_ID: executionId },
cwd: '/opt/mcp/sandbox',
uid: 1000, // Drop to unprivileged user
gid: 1000,
detached: false,
});
// Apply runtime constraints
proc.stdout?.on('data', (chunk) => this.validateOutput(chunk, executionId));
proc.stderr?.on('data', (chunk) => this.handleSandboxError(chunk, executionId));
this.activeProcesses.set(executionId, proc);
return executionId;
}
private validateOutput(chunk: Buffer, id: string): void {
// Intercept raw output before it reaches the context plane
const raw = chunk.toString();
if (raw.includes('ssh') || raw.includes('PRIVATE KEY')) {
throw new Error(Sandbox violation: sensitive path access detected in ${id});
}
}
async terminate(id: string): Promise<void> {
const proc = this.activeProcesses.get(id);
if (proc) {
proc.kill('SIGTERM');
this.activeProcesses.delete(id);
}
}
}
**Why this choice:** Dropping UID/GID and restricting `cwd` prevents filesystem traversal. Intercepting stdout/stderr at the pipe level ensures malicious payloads never reach the LLM's token distribution plane.
### 2. Context Sanitization & Schema Enforcement
Tool responses must pass through a strict transformer that escapes control characters, enforces JSON schema boundaries, and strips executable metadata. Raw appending is replaced with structured context injection.
```typescript
import { z } from 'zod';
const ToolResponseSchema = z.object({
tool_name: z.string().min(1).max(64),
status: z.enum(['success', 'partial', 'error']),
payload: z.record(z.unknown()),
trace_id: z.string().uuid(),
});
export class ContextSanitizer {
static sanitize(rawInput: unknown): z.infer<typeof ToolResponseSchema> {
const parsed = ToolResponseSchema.safeParse(rawInput);
if (!parsed.success) {
throw new Error('Context injection blocked: invalid tool response schema');
}
const safe = parsed.data;
// Neutralize control sequences and markdown injection vectors
safe.payload = this.escapeControlChars(safe.payload);
safe.trace_id = crypto.randomUUID(); // Rotate trace to prevent session fixation
return safe;
}
private static escapeControlChars(obj: Record<string, unknown>): Record<string, unknown> {
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
sanitized[key] = value.replace(/[\x00-\x1F\x7F-\x9F]/g, '');
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = this.escapeControlChars(value as Record<string, unknown>);
} else {
sanitized[key] = value;
}
}
return sanitized;
}
}
Why this choice: Zod validation guarantees structural integrity before context injection. Control character stripping neutralizes prompt injection and markdown-based execution tricks. Trace rotation prevents session fixation across multi-turn agentic loops.
3. Dependency Pinning & Cryptographic Verification
Dynamic resolution (npx -y) must be replaced with frozen manifests and signature verification. Every tool binary or script is hashed against a trusted registry before execution.
import { createHash, createVerify } from 'crypto';
import { readFileSync } from 'fs';
interface DependencyManifest {
tool: string;
version: string;
sha256: string;
signature: string;
}
export class SupplyChainVerifier {
private trustedRegistry: Map<string, DependencyManifest> = new Map();
register(manifest: DependencyManifest): void {
this.trustedRegistry.set(manifest.tool, manifest);
}
async verify(toolPath: string): Promise<boolean> {
const buffer = readFileSync(toolPath);
const hash = createHash('sha256').update(buffer).digest('hex');
const manifest = this.trustedRegistry.get(toolPath.split('/').pop()!);
if (!manifest || manifest.sha256 !== hash) {
throw new Error(`Dependency mismatch: ${toolPath} hash verification failed`);
}
const verifier = createVerify('SHA256');
verifier.update(buffer);
const isValid = verifier.verify(
readFileSync('keys/mcp-tool-signing.pem', 'utf8'),
manifest.signature,
'base64'
);
if (!isValid) {
throw new Error(`Signature invalid: ${toolPath} may be tampered`);
}
return true;
}
}
Why this choice: Cryptographic pinning eliminates supply chain weaponization. SHA256 hashing combined with asymmetric signature verification ensures that only audited, unmodified binaries execute.
4. Explicit Routing Gateway
Cross-server calls must never be routed implicitly through the LLM. A deterministic gateway intercepts routing requests, validates them against an allowlist, and requires explicit consent before forwarding.
interface RoutingRequest {
sourceTool: string;
targetTool: string;
action: string;
payload: unknown;
}
export class RoutingGateway {
private allowlist: Set<string> = new Set();
registerRoute(source: string, target: string, action: string): void {
this.allowlist.add(`${source}::${target}::${action}`);
}
async authorize(request: RoutingRequest): Promise<boolean> {
const routeKey = `${request.sourceTool}::${request.targetTool}::${request.action}`;
if (!this.allowlist.has(routeKey)) {
console.warn(`[ROUTING] Blocked unauthorized cross-tool call: ${routeKey}`);
return false;
}
return true;
}
}
Why this choice: Deterministic routing removes the LLM from the security decision plane. Explicit allowlists prevent lateral movement and ensure every cross-tool interaction is auditable.
Pitfall Guide
1. Implicit Privilege Inheritance
Explanation: MCP servers launched as child processes automatically inherit the host's environment variables, filesystem permissions, and network tokens. Attackers leverage this to read SSH keys, API credentials, or configuration files.
Fix: Execute all tools under dedicated service accounts with dropped privileges. Mount only explicitly required directories using read-only filesystem overlays.
2. Raw Context Appending
Explanation: Tool outputs injected directly into the prompt bypass sanitization layers. Malicious payloads embedded in responses can trigger prompt injection, command execution, or context window poisoning.
Fix: Implement a strict output transformer that validates JSON schema, escapes control sequences, and enforces token budget limits before context injection.
3. Dynamic Dependency Resolution
Explanation: Using npx -y or unpinned package managers allows attackers to substitute compromised versions during runtime. The LLM's tool discovery mechanism blindly trusts the latest registry entry.
Fix: Lock dependencies to specific versions, verify SHA256 checksums, and require cryptographic signatures before execution. Maintain a frozen validation log for auditability.
4. LLM-as-Router Assumption
Explanation: Treating the foundation model as a secure dispatcher for cross-server communication enables unauthenticated lateral movement. The model can silently route destructive commands to connected tools while omitting traces from the chat history.
Fix: Deploy a deterministic routing gateway that intercepts all cross-tool calls, validates them against an explicit allowlist, and logs every routing decision independently of the conversational state.
5. Flat Context Windows
Explanation: Allowing unbounded tool history and RAG sources to accumulate in the context window creates a poisoning surface. Attackers inject malicious metadata that persists across turns, influencing subsequent model decisions.
Fix: Enforce context window caps, implement rolling summaries for historical tool outputs, and isolate RAG sources in separate vector namespaces with strict retrieval quotas.
6. Schema Blindness
Explanation: Accepting arbitrary tool metadata and response structures enables tool schema confusion. Malicious tools can inject unexpected fields that override system prompts or trigger unintended execution paths.
Fix: Apply strict JSON Schema validation on both request and response payloads. Reject any payload that deviates from the declared contract, and log schema violations for security review.
7. Silent Multi-Turn Persistence
Explanation: Agentic loops running without state verification allow attackers to maintain persistence across turns. Compromised tools can execute delayed payloads or escalate privileges incrementally without triggering alerts.
Fix: Implement turn limits, state verification tokens, and explicit loop termination conditions. Require cryptographic acknowledgment for each iteration to prevent unauthorized continuation.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single-Tool Local Agent | Lightweight sandbox + schema sanitizer | Minimal overhead, sufficient for isolated workflows | Low (CPU/RAM increase ~15%) |
| Multi-Tool Enterprise Orchestrator | Full routing gateway + dependency pinning + context isolation | Prevents lateral movement and supply chain compromise at scale | Medium (Infrastructure + monitoring overhead) |
| Untrusted Third-Party Tools | Strict allowlist routing + cryptographic verification + network isolation | Neutralizes malicious payloads and prevents host takeover | High (Dedicated sandbox clusters + audit logging) |
Configuration Template
// mcp-security-config.ts
import { ToolExecutionSandbox } from './sandbox';
import { ContextSanitizer } from './sanitizer';
import { SupplyChainVerifier } from './verifier';
import { RoutingGateway } from './gateway';
export const MCP_SECURITY_PROFILE = {
sandbox: new ToolExecutionSandbox(),
sanitizer: ContextSanitizer,
verifier: new SupplyChainVerifier(),
gateway: new RoutingGateway(),
runtime: {
maxContextTokens: 8192,
turnLimit: 12,
networkIsolation: true,
filesystemQuotaMB: 256,
},
allowedRoutes: [
'file-reader::data-parser::extract',
'data-parser::model-inference::predict',
'model-inference::response-formatter::render',
],
};
// Initialize security layers
MCP_SECURITY_PROFILE.verifier.register({
tool: 'file-reader',
version: '1.4.2',
sha256: 'a1b2c3d4e5f6...',
signature: 'base64-signature-here',
});
MCP_SECURITY_PROFILE.gateway.registerRoute('file-reader', 'data-parser', 'extract');
MCP_SECURITY_PROFILE.gateway.registerRoute('data-parser', 'model-inference', 'predict');
MCP_SECURITY_PROFILE.gateway.registerRoute('model-inference', 'response-formatter', 'render');
Quick Start Guide
- Initialize pinned manifest: Generate a
manifest.json containing tool names, exact versions, SHA256 hashes, and cryptographic signatures. Load it into the SupplyChainVerifier before runtime.
- Deploy sandbox runtime: Configure the
ToolExecutionSandbox with UID/GID dropping, read-only filesystem mounts, and network isolation flags. Verify that child processes cannot access host credentials.
- Configure bridge with sanitizer: Instantiate the
ContextSanitizer and route all tool outputs through it before context injection. Set maxContextTokens and turnLimit to enforce boundaries.
- Validate with test payload: Execute a controlled tool invocation that returns structured JSON. Confirm that schema validation passes, control characters are stripped, and routing gates block unauthorized cross-tool calls.
- Enable audit logging: Route all sandbox violations, schema rejections, and routing denials to a centralized security log. Set up alerting for repeated violations or hash mismatches.