Paquete de defensa en profundidad para servidores MCP stdio: wrappers guardExec/guardSpawn de sustitución directa, CLI de auditoría AST, servidor MCP de referencia. Cierra la clase stdio-RCE de los 200 000 servidores de Ox-Security (LiteLLM CVE-2025-69256). MIT, TypeScript, Node >= 20.
Parte del StudioMeyer MCP Stack — Hecho en Mallorca 🌴 · ⭐ si lo usas
Paquete de defensa en profundidad para servidores MCP stdio. Envuelve child_process.exec/spawn con lista blanca + sandbox + detección de replay, además de una CLI de auditoría AST (mcp-shellguard-audit) que escanea el código fuente de servidores MCP en busca de llamadas shell sin sanitizar. Cierra la clase de vulnerabilidad MCP stdio-RCE señalada por Ox-Security (200k servidores vulnerables, divulgación de mayo de 2026).
@modelcontextprotocol/sdk ^1.29.0npm install mcp-stdio-shellguard
O ejecuta la CLI de auditoría directamente sin instalar:
npx -y -p mcp-stdio-shellguard mcp-shellguard-audit scan ./src
Tres capas, opt-in por piezas:
guardExec / guardSpawn como reemplazo directo que puedes invocar desde tu propio servidor MCP. Lista blanca de denegación por defecto, perfiles de sandbox y ventana de replay.mcp-shellguard-audit scan <path> recorre el AST e informa de 12 antipatrones, desde LOW (sin timeout) hasta CRITICAL (exec(\...${userInput}...`)`).mcp-stdio-shellguard-demo expone 8 herramientas para que MCP Inspector / Claude Desktop puedan manejar el paquete directamente.| Herramienta | Tipo | Propósito |
|---|---|---|
guard_exec | destructivo | child_process.exec defendido. Fuerza el vector args[], lista blanca + sandbox + replay. Devuelve stdout, stderr, exitCode, canonicalHash, isReplay, trustTier. |
guard_spawn | destructivo | child_process.spawn defendido. Devuelve hashes SHA-256 de stdout/stderr en lugar de los cuerpos completos. Rechaza de forma tajante shell:true. |
register_allowlist | mutador | Registra un nombre de herramienta con ejecutable + regex de argumentos. Sin registro, se aplica la denegación por defecto. |
audit_source | solo lectura | Escanea una ruta TS/JS en busca de antipatrones de inyección shell. Devuelve AuditFinding[] + resumen. |
audit_report | solo lectura | Formatea un resultado de auditoría como markdown / json / SARIF 2.1.0. |
replay_check | solo lectura | Calcula el hash SHA-256 canónico de una invocación e informa de si ya está en la ventana de replay. |
sandbox_status | solo lectura | Informa del perfil de sandbox activo, los límites concretos y el indicador de cgroup-v2 activo. |
trust_tier | solo lectura | Deriva el nivel LOW/MEDIUM/HIGH/CRITICAL para una herramienta registrada, además de sugerencias de mejora. |
| Perfil | Timeout | Máx. stdout | Máx. stderr | Presupuesto de FD | cgroup-v2 |
|---|---|---|---|---|---|
strict | 5 s | 1 MB | 256 KB | 32 | sí (cpu/memoria) |
standard (predeterminado) | 30 s | 10 MB | 1 MB | 256 | sí |
permissive | 5 min | 100 MB | 10 MB | 1024 | no |
Quien llama puede ajustar a la baja mediante timeoutMs / fdBudget por llamada, pero no puede ampliar los límites más allá del perfil.
| Nivel | Condición |
|---|---|
| LOW | herramienta no registrada (denegación por defecto) |
| MEDIUM | registrada pero con argsPatterns vacío (se permiten argumentos arbitrarios) |
| HIGH | argsPatterns definido pero con sandbox o rastreador de replay inactivos |
| CRITICAL | argsPatterns + sandbox + replay todos activos |
Sube de LOW → CRITICAL registrando la herramienta, definiendo argsPatterns y ejecutándola a través de guardExec/guardSpawn (que siempre activan sandbox + replay).
import {
AllowlistRegistry,
ReplayWindow,
guardExec,
} from "mcp-stdio-shellguard";
const registry = new AllowlistRegistry();
const replay = new ReplayWindow();
registry.register({
toolName: "git-log",
executable: "/usr/bin/git",
argsPatterns: ["^log$", "^--oneline$", "^-n$", "^\\d+$"],
sandboxProfile: "strict",
});
const result = await guardExec(
{
toolName: "git-log",
command: "/usr/bin/git",
args: ["log", "--oneline", "-n", "10"],
},
{ registry, replay },
);
console.log(result.stdout); // → commit lines
console.log(result.trustTier); // → "CRITICAL"
console.log(result.canonicalHash); // → 64-char SHA-256
mcp-shellguard-audit scan ./src
mcp-shellguard-audit scan ./src --format sarif --output audit.sarif
mcp-shellguard-audit scan ./src --severity-floor HIGH # CI gate
Códigos de salida:
0 limpio (sin hallazgos en o por encima del umbral)1 hay hallazgos2 errores de análisis / E/S| ID | Gravedad | Se dispara con |
|---|---|---|
exec_template_literal_with_input | CRITICAL | child_process.exec(\ls ${x}`)` |
exec_dynamic_string | CRITICAL | child_process.exec(cmd) |
exec_sync_dynamic_string | CRITICAL | child_process.execSync(cmd) |
eval_near_child_process | CRITICAL | eval(...) |
function_constructor_near_child_process | CRITICAL | new Function(...) |
spawn_dynamic_file_args | HIGH | spawn(bin, userArgs) |
exec_file_dynamic | HIGH | execFile(bin, ...) |
shell_true_option | HIGH | { shell: true } |
os_system_equivalent | HIGH | Deno.run / Bun.spawn |
spawn_literal_dynamic_args | MEDIUM | spawn('git', userArgs) |
unbounded_buffer | LOW | exec sin maxBuffer |
missing_timeout | LOW | exec/spawn sin timeout |
El escáner resuelve los enlaces renombrados de child_process antes de comparar, por lo que las formas peligrosas siguientes se detectan incluso cuando la llamada pasa por un alias en lugar de un child_process.exec literal:
const execAsync = promisify(exec); execAsync(...${x})import cp from "node:child_process"; cp.exec(...${x})const { exec: sh } = require("child_process"); sh(...${x})import { exec as run } from "node:child_process"; run(...)Las variantes síncronas (spawnSync, execFileSync) comparten sus reglas asíncronas, y shell_true_option también se activa con un shell en forma de cadena ({ shell: "/bin/sh" }) o con un valor de shell dinámico — no solo con el literal { shell: true }. Un promisify de una función que no sea de child_process, una desestructuración de otro módulo y { shell: false } permanecen limpios (sin falsos positivos).
// shellguard:ignore-next-line — suprime un hallazgo// shellguard:ignore-file — suprime el archivo completo (poco habitual; prefiere la opción por línea)