Hands-On Tutorial: Building Zero-Trust Sandbox Firewalls for Model Context Protocol (MCP)
Executive Summary (BLUF)
The Model Context Protocol (MCP) standardizes how Large Language Model (LLM) agents interface with external tools, local file systems, databases, and APIs. However, because MCP operates over stateful JSON-RPC 2.0 channels (via stdio, Server-Sent Events, or WebSockets), traditional Web Application Firewalls (WAFs) and HTTP layer security controls cannot inspect or sanitize payload parameters dynamically.
A Zero-Trust MCP Sandbox Firewall acts as an inline security proxy positioned between the AI Client (such as Claude Desktop, Cursor, or custom agent runtimes) and the target MCP Server. By enforcing strict JSON Schema parameter validation, real-time payload sanitization, command injection heuristics, strict tool access policies, and isolation semantics, an MCP firewall prevents Indirect Prompt Injection (IPI), unauthorized file exfiltration, and arbitrary code execution.
This hands-on guide provides a step-by-step implementation of an enterprise-grade Zero-Trust MCP Firewall in TypeScript/Node.js, including complete source code, security policy configurations, and unit verification tests.
Step-0: Architecture & Mental Models of MCP Communication
Before securing MCP traffic, security engineers must understand the low-level JSON-RPC 2.0 protocol mechanics powering AI agent tool invocations.
+------------------+ JSON-RPC Request +-------------------------+ Inspected Request +------------------+
| | -------------------------------> | | -------------------------------> | |
| AI Agent Host | | Zero-Trust MCP Firewall | | Target MCP |
| (Claude/Cursor) | <------------------------------- | (Inline Security) | <------------------------------- | Server |
+------------------+ Sanitized Response +-------------------------+ Filtered Response +------------------+
|
v
+-------------------+
| Security Log & |
| Audit Store (JSON)|
+-------------------+
1. Protocol Transport Mechanics
MCP supports two primary transport layers:
- stdio (Standard I/O): The host application launches the MCP server as a child process and communicates via standard input (
stdin) and standard output (stdout). Each message is a newline-delimited JSON-RPC 2.0 object. - HTTP with Server-Sent Events (SSE): The client opens an HTTP GET connection to an SSE endpoint to receive server events, and sends client messages via HTTP POST requests containing JSON-RPC payloads.
2. Core JSON-RPC 2.0 Message Types
An MCP session involves three primary interactions:
- Capability Negotiation (
initialize): Exchanging protocol versions and client/server capabilities. - Tool Discovery (
tools/list): The server exposes available functions, descriptions, and JSON Schemas defining expected input parameters. - Tool Execution (
tools/call): The AI client issues a call request with arguments matching the tool's schema:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "execute_shell_command",
"arguments": {
"command": "ls -la; cat /etc/passwd"
}
}
}
3. The Security Vulnerability: Indirect Prompt Injection (IPI)
Indirect Prompt Injection occurs when an LLM ingests untrusted external text (from a web page, file, or database query result) containing adversarial instructions designed to hijack control flow. If the LLM subsequently invokes an MCP tool with malicious arguments (e.g., executing shell payloads or reading sensitive SSH keys), unmonitored MCP execution leads to catastrophic system compromise.
The Threat Model & Attack Vectors
| Attack Vector | Mechanism | Impact | Firewall Countermeasure |
|---|---|---|---|
| Command Injection | Appending shell operators (;, ` | , &&, `` ``) in tool parameters. | Remote Code Execution (RCE) on host machine. |
| Path Traversal | Using ../ or absolute paths (/etc/shadow, C:\Windows) in file read tools. | Unauthorized data exfiltration & privilege disclosure. | Path canonicalization & strict root folder jail check. |
| Indirect Prompt Injection | Embedded prompt text tricking LLM into invoking unapproved tools. | Exfiltration of user instructions or credentials via outbound requests. | Tool whitelist policy & strict capability scopes. |
| Resource Exhaustion | Flooding MCP server with high-frequency complex tool requests. | Denial of Service (DoS) of agent operations. | Token Bucket Rate Limiting per session. |
Step 1: Designing the Zero-Trust Security Policy Engine
A robust security policy defines allowable tool names, required parameter rules, forbidden regex patterns, and maximum payload limits. We formalize this policy in JSON structure:
{
"version": "1.0",
"defaultPolicy": "DENY",
"rateLimit": {
"maxRequestsPerMinute": 30
},
"allowedTools": {
"read_file": {
"enabled": true,
"allowedPaths": ["/workspace", "/app/data"],
"forbiddenPatterns": ["\\.\\.", "/etc/", "~/", "\\.env"]
},
"execute_command": {
"enabled": true,
"allowedCommands": ["git", "npm", "ls"],
"forbiddenCharacters": [";", "|", "&", "`", "$", "(", ")", "<", ">"]
}
}
}
Step 2: Implementation of the MCP Proxy Firewall
Below is the complete, runnable TypeScript implementation of an inline MCP Proxy Firewall (mcp-firewall.ts). It intercepts stdio traffic between the AI host and child MCP server, performing inline inspection and policy enforcement.
import { spawn, ChildProcess } from 'child_process';
import * as readline from 'readline';
interface FirewallConfig {
allowedTools: Set<string>;
restrictedPaths: string[];
forbiddenPatterns: RegExp[];
maxPayloadSize: number;
}
const defaultConfig: FirewallConfig = {
allowedTools: new Set(['read_file', 'list_dir', 'execute_command']),
restrictedPaths: ['/etc', '/var', '/root', '/home/cyb3rvolt3x/.ssh', '.env'],
forbiddenPatterns: [
/;\s*/, // Command chaining ;
/\|\s*/, // Piping |
/`[^`]*`/, // Backticks `command`
/\$\([^)]*\)/, // Subshell $(command)
/\.\.\//, // Path traversal ../
/cat\s+\/etc\/passwd/i // Sensitive file read attempts
],
maxPayloadSize: 64 * 1024 // 64 KB limit
};
export class MCPProxyFirewall {
private childProcess: ChildProcess | null = null;
private config: FirewallConfig;
private requestCount: number = 0;
constructor(targetCommand: string, targetArgs: string[], config: FirewallConfig = defaultConfig) {
this.config = config;
this.spawnTargetServer(targetCommand, targetArgs);
this.setupStdioInterception();
}
private spawnTargetServer(command: string, args: string[]) {
this.childProcess = spawn(command, args, {
stdio: ['pipe', 'pipe', 'inherit']
});
this.childProcess.on('exit', (code) => {
console.error(`[MCP Firewall] Target process exited with code ${code}`);
process.exit(code || 0);
});
}
private setupStdioInterception() {
if (!this.childProcess || !this.childProcess.stdout || !this.childProcess.stdin) {
throw new Error('[MCP Firewall] Failed to attach stdio pipes');
}
// Intercept Input from AI Client (stdin -> childProcess.stdin)
const clientRl = readline.createInterface({
input: process.stdin,
terminal: false
});
clientRl.on('line', (line) => {
this.handleClientMessage(line);
});
// Forward Output from MCP Server (childProcess.stdout -> stdout)
const serverRl = readline.createInterface({
input: this.childProcess.stdout,
terminal: false
});
serverRl.on('line', (line) => {
// Forward server responses to client
process.stdout.write(line + '\n');
});
}
private handleClientMessage(rawLine: string) {
if (rawLine.length > this.config.maxPayloadSize) {
this.sendErrorResponse(null, -32600, 'Payload size exceeds maximum allowed threshold');
return;
}
let payload: any;
try {
payload = JSON.parse(rawLine);
} catch (e) {
this.sendErrorResponse(null, -32700, 'Invalid JSON payload');
return;
}
// Inspect tools/call requests
if (payload.method === 'tools/call') {
const isAllowed = this.inspectToolCall(payload);
if (!isAllowed) {
return; // Blocked; error response sent
}
}
// Request passed security audit; forward to backend MCP server
if (this.childProcess && this.childProcess.stdin) {
this.childProcess.stdin.write(rawLine + '\n');
}
}
private inspectToolCall(payload: any): boolean {
const requestId = payload.id;
const toolName = payload.params?.name;
const args = payload.params?.arguments || {};
// 1. Tool Whitelist Audit
if (!this.config.allowedTools.has(toolName)) {
this.logAudit('BLOCKED_UNAUTHORIZED_TOOL', payload);
this.sendErrorResponse(requestId, -32601, `Tool '${toolName}' is forbidden by firewall policy`);
return false;
}
// 2. Inspection of Arguments for Injection Patterns
const serializedArgs = JSON.stringify(args);
for (const pattern of this.config.forbiddenPatterns) {
if (pattern.test(serializedArgs)) {
this.logAudit('BLOCKED_MALICIOUS_PATTERN', { toolName, pattern: pattern.toString(), args });
this.sendErrorResponse(requestId, -32000, `Blocked: Malicious parameter pattern detected matching '${pattern}'`);
return false;
}
}
// 3. Path Traversal & Restricted Directory Audit
if (args.path || args.filePath || args.filename) {
const targetPath = String(args.path || args.filePath || args.filename);
for (const restricted of this.config.restrictedPaths) {
if (targetPath.includes(restricted)) {
this.logAudit('BLOCKED_RESTRICTED_PATH', { toolName, targetPath, restricted });
this.sendErrorResponse(requestId, -32001, `Access Denied: Path '${targetPath}' touches restricted resource '${restricted}'`);
return false;
}
}
}
this.logAudit('ALLOWED_TOOL_CALL', { toolName });
return true;
}
private sendErrorResponse(id: any, code: number, message: string) {
const errorResponse = {
jsonrpc: '2.0',
id: id,
error: {
code: code,
message: message
}
};
process.stdout.write(JSON.stringify(errorResponse) + '\n');
}
private logAudit(event: string, details: any) {
const auditRecord = {
timestamp: new Date().toISOString(),
event,
details
};
console.error(`[MCP FIREWALL AUDIT] ${JSON.stringify(auditRecord)}`);
}
}
// Entrypoint Execution
if (require.main === module) {
const targetCmd = process.argv[2] || 'node';
const targetArgs = process.argv.slice(3);
if (!targetCmd) {
console.error('Usage: node mcp-firewall.js <target_mcp_command> [args...]');
process.exit(1);
}
new MCPProxyFirewall(targetCmd, targetArgs);
}
Step 3: Verifying Firewall Enforcement with Test Scenarios
To validate our zero-trust firewall against offensive payloads, we execute a suite of verification test vectors.
Test Case 1: Benign Tool Call Execution
Client Input:
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/workspace/readme.txt"}}}
Firewall Action: ALLOWED_TOOL_CALL. Payload forwarded to target MCP server.
Test Case 2: Command Injection Attempt
Client Input:
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"execute_command","arguments":{"command":"ls -la; cat /etc/passwd"}}}
Firewall Action: BLOCKED_MALICIOUS_PATTERN. Firewall blocks request inline and responds:
{"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"Blocked: Malicious parameter pattern detected matching '/;\\s*/'"}}
Test Case 3: Unauthorized Restricted Path Access
Client Input:
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/home/cyb3rvolt3x/.ssh/id_rsa"}}}
Firewall Action: BLOCKED_RESTRICTED_PATH. Firewall responds with -32001 Access Denied.
Production Hardening & Enterprise Deployment Checklist
When deploying an MCP Firewall in enterprise production environments, apply the following defense-in-depth principles:
- Process Namespace Sandboxing: Wrap child MCP processes in isolated Linux cgroups/namespaces (or Docker containers using
--cap-drop=ALL --read-only). - Dynamic JSON Schema Validation: Validate tool call arguments against the exact JSON Schema returned by
tools/listduring capability initialization. - Mutual TLS (mTLS) for Network Transports: For SSE/WebSocket transports, require strict mTLS certificate validation between the client, proxy, and MCP server.
- Structured SIEM Integration: Ship audit logs (
[MCP FIREWALL AUDIT]) to centralized SIEM platforms (Splunk, Elastic, Datadog) for anomaly detection and alert triggering.
Key Takeaways
- MCP Security requires inline payload inspection: WAFs fail on MCP because MCP communicates via stateful streams (
stdio/SSE). - Zero-Trust Enforcement: Never trust LLM parameter generation; enforce strict schemas, parameter regex checks, and path canonicalization.
- Defense-in-Depth: Combine inline proxy firewalling with container isolation and structured SIEM auditing to ensure resilient AI agent architectures.
