Pacote de defesa em profundidade para servidores MCP stdio: wrappers guardExec/guardSpawn de substituição, CLI de auditoria AST, servidor MCP de referência. Fecha a classe de RCE via stdio de 200k servidores do Ox-Security (CVE-2025-69256 do LiteLLM). MIT, TypeScript, Node >= 20.
Parte do StudioMeyer MCP Stack — Construído em Mallorca 🌴 · ⭐ se você usar
com allowlist + sandbox + detecção de replay, além de uma CLI de auditoria AST (mcp-shellguard-audit)
que escaneia os códigos-fonte de servidores MCP em busca de chamadas de shell não sanitizadas. Fecha a classe
de RCE via stdio em MCP da Ox-Security (200 mil servidores vulneráveis, divulgação de maio de 2026).
@modelcontextprotocol/sdk ^1.29.0npm install mcp-stdio-shellguard
Ou execute a CLI de auditoria diretamente sem instalar:
npx -y -p mcp-stdio-shellguard mcp-shellguard-audit scan ./src
Três camadas, opcionais e ativáveis individualmente:
guardExec / guardSpawn prontos para uso, chamados a partir do seu
próprio servidor MCP. Allowlist com negação por padrão, perfis de sandbox, janela de replay.mcp-shellguard-audit scan <path> percorre a AST e reporta
12 anti-padrões, de LOW (no timeout) a CRITICAL (exec(\...${userInput}...`)`).mcp-stdio-shellguard-demo expõe 8 ferramentas
para que o MCP Inspector / Claude Desktop possam operar o pacote diretamente.| Ferramenta | Tipo | Finalidade |
|---|---|---|
guard_exec | destrutiva | child_process.exec protegido. Força vetor args[], allowlist + sandbox + replay. Retorna stdout, stderr, exitCode, canonicalHash, isReplay, trustTier. |
guard_spawn | destrutiva | child_process.spawn protegido. Retorna hashes SHA-256 de stdout/stderr em vez dos corpos completos. Rejeita firmemente shell:true. |
register_allowlist | mutante | Registra um nome de ferramenta com executável + regex de args. Sem registro, aplica-se a negação por padrão. |
audit_source | somente leitura | Escaneia um caminho TS/JS em busca de anti-padrões de injeção de shell. Retorna AuditFinding[] + resumo. |
audit_report | somente leitura | Formata um resultado de auditoria como markdown / json / SARIF 2.1.0. |
replay_check | somente leitura | Calcula o hash SHA-256 canônico de uma invocação e informa se ela já está na janela de replay. |
sandbox_status | somente leitura | Reporta o perfil de sandbox ativo + limites concretos + flag de cgroup-v2 ativo. |
trust_tier | somente leitura | Deriva o nível LOW/MEDIUM/HIGH/CRITICAL de uma ferramenta registrada, além de dicas de melhoria. |
| Perfil | Timeout | stdout máx. | stderr máx. | Orçamento de FD | cgroup-v2 |
|---|---|---|---|---|---|
strict | 5 s | 1 MB | 256 KB | 32 | sim (cpu/memory) |
standard (padrão) | 30 s | 10 MB | 1 MB | 256 | sim |
permissive | 5 min | 100 MB | 10 MB | 1024 | não |
O chamador pode restringir via timeoutMs / fdBudget por chamada. O chamador não pode ampliar
além do perfil.
| Nível | Condição |
|---|---|
| LOW | ferramenta não registrada (negação por padrão) |
| MEDIUM | registrada, mas argsPatterns vazio (quaisquer args permitidos) |
| HIGH | argsPatterns definido, mas sandbox ou rastreador de replay inativo |
| CRITICAL | argsPatterns + sandbox + replay todos ativos |
Eleve de LOW → CRITICAL registrando a ferramenta + definindo argsPatterns + executando
através de guardExec/guardSpawn (que sempre ativam 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 saída:
0 limpo (nenhum achado no nível mínimo ou acima)1 achados presentes2 erros de análise / E/SignoreGlobs personalizados passados para scanPath ou para a ferramenta MCP audit_source aceitam
no máximo 128 strings. Cada string é limitada a 1024 unidades de código UTF-16 e 16
caracteres de agrupamento de abertura não escapados ((, {, [). Entradas acima desses
limites geram um erro controlado antes da correspondência.
| ID | Severidade | Dispara em |
|---|---|---|
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 sem maxBuffer |
missing_timeout | LOW | exec/spawn sem timeout |
O scanner resolve vinculações de child_process renomeadas antes da correspondência,
de modo que as formas perigosas abaixo são detectadas mesmo quando a chamada passa por um
alias em vez de um 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(...)As variantes síncronas (spawnSync, execFileSync) compartilham suas regras assíncronas,
e shell_true_option também dispara em um shell de string ({ shell: "/bin/sh" })
ou em um valor de shell dinâmico — não apenas no literal { shell: true }. Um
promisify de uma função que não é de child_process, uma desestruturação de outro
módulo e { shell: false } permanecem limpos (sem falsos positivos).
// shellguard:ignore-next-line — suprime um achado// shellguard:ignore-file — suprime o arquivo inteiro (raro; prefira por linha)