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 — Creado en Mallorca 🌴 · ⭐ si lo usas
con lista de permitidos + 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 a shell sin sanear. Cierra la clase de RCE
por stdio en MCP de Ox-Security (200k servidores vulnerables, divulgación de mayo de 2026).
@modelcontextprotocol/sdk ^1.29.0npm install mcp-stdio-shellguard
O ejecute la CLI de auditoría directamente sin instalarlo:
npx -y -p mcp-stdio-shellguard mcp-shellguard-audit scan ./src
Tres capas, opcionales por partes:
guardExec / guardSpawn listos para usar que llamas desde tu
propio servidor MCP. Lista de permitidos con denegación por defecto, perfiles de sandbox, ventana de replay.mcp-shellguard-audit scan <path> recorre el AST, informa
12 anti-patrones desde LOW (no 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 | destructiva | child_process.exec defendido. Fuerza el vector args[], lista de permitidos + sandbox + replay. Devuelve stdout, stderr, exitCode, canonicalHash, isReplay, trustTier. |
guard_spawn | destructiva | child_process.spawn defendido. Devuelve hashes SHA-256 de stdout/stderr en lugar de los cuerpos completos. Rechaza de forma estricta shell:true. |
register_allowlist | mutante | Registra un nombre de herramienta con ejecutable + regex de args. Sin registro se aplica la denegación por defecto. |
audit_source | solo lectura | Escanea una ruta TS/JS en busca de anti-patrones de inyección de 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 si ya está en la ventana de replay. |
sandbox_status | solo lectura | Informa el perfil de sandbox activo + límites concretos + indicador de cgroup-v2 activo. |
trust_tier | solo lectura | Deriva el nivel LOW/MEDIUM/HIGH/CRITICAL para una herramienta registrada más sugerencias de mejora. |
| Perfil | Tiempo de espera | stdout máx. | stderr máx. | Presupuesto de FD | cgroup-v2 |
|---|---|---|---|---|---|
strict | 5 s | 1 MB | 256 KB | 32 | sí (cpu/memory) |
standard (predeterminado) | 30 s | 10 MB | 1 MB | 256 | sí |
permissive | 5 min | 100 MB | 10 MB | 1024 | no |
El llamador puede restringir mediante timeoutMs / fdBudget por llamada. El llamador no puede ampliar
más allá del perfil.
| Nivel | Condición |
|---|---|
| LOW | herramienta no registrada (denegación por defecto) |
| MEDIUM | registrada pero argsPatterns vacío (se permite cualquier args) |
| HIGH | argsPatterns establecido pero sandbox o rastreador de replay inactivos |
| CRITICAL | argsPatterns + sandbox + replay todos activos |
Eleva de LOW → CRITICAL registrando la herramienta + estableciendo argsPatterns + ejecutando
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 hallazgos presentes2 errores de análisis / E/SLos ignoreGlobs personalizados pasados a scanPath o a la herramienta MCP audit_source aceptan
como máximo 128 cadenas. Cada cadena está limitada a 1024 unidades de código UTF-16 y 16
caracteres de apertura de agrupación sin escapar ((, {, [). Las entradas que superan estos
límites generan un error controlado antes de la coincidencia.
| ID | Severidad | Se activa 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 de child_process renombrados antes de la coincidencia,
por lo que las formas peligrosas a continuación 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 de cadena ({ shell: "/bin/sh" })
o un valor de shell dinámico — no solo con el literal { shell: true }. Un
promisify de una función que no es de child_process, una desestructuración de otro
módulo y { shell: false } permanecen limpios (sin falsos positivos).