
mcp-stdio-shellguard v0.1.2
Bundle de défense en profondeur pour serveurs MCP stdio : wrappers guardExec/guardSpawn prêts à l'emploi, CLI d'audit AST, serveur MCP de référence. Corrige la classe de vulnérabilité stdio-RCE Ox-Security 200k-server (LiteLLM CVE-2025-69256). MIT, TypeScript, Node >= 20.
Fait partie de la StudioMeyer MCP Stack — Conçu à Majorque 🌴 · ⭐ si vous l'utilisez
mcp-stdio-shellguard
Bundle de défense en profondeur pour les serveurs MCP stdio. Enveloppe `child_process.exec/spawn`avec allowlist + sandbox + détection de rejeu, plus une CLI d'audit AST (mcp-shellguard-audit)
qui analyse les sources des serveurs MCP à la recherche d'appels shell non assainis. Comble la classe
de RCE stdio MCP d'Ox-Security (200k serveurs vulnérables, divulgation de mai 2026).
- Spécification MCP : 2025-06-18
- SDK :
@modelcontextprotocol/sdk^1.29.0 - Node : >= 20
- Licence : MIT
- Auteur : Matthias Meyer (StudioMeyer)
Installation
npm install mcp-stdio-shellguard
Ou exécutez directement la CLI d'audit sans installation :
npx -y -p mcp-stdio-shellguard mcp-shellguard-audit scan ./src
Ce que cela vous apporte
Trois couches, activables individuellement :
- API de bibliothèque —
guardExec/guardSpawnprêts à l'emploi que vous appelez depuis votre propre serveur MCP. Liste d'autorisation par défaut en refus, profils de bac à sable, fenêtre de rejeu. - CLI d'audit —
mcp-shellguard-audit scan <path>parcourt l'AST, signale 12 anti-patterns allant de LOW (no timeout) à CRITICAL (exec(\...${userInput}...`)`). - Serveur MCP de référence —
mcp-stdio-shellguard-demoexpose 8 outils afin que le MCP Inspector / Claude Desktop puissent piloter le bundle directement.
Outils (serveur de référence)
| Outil | Type | Objectif |
|---|---|---|
guard_exec | destructif | child_process.exec protégé. Force le vecteur args[], liste d'autorisation + bac à sable + rejeu. Renvoie stdout, stderr, exitCode, canonicalHash, isReplay, trustTier. |
guard_spawn | destructif | child_process.spawn protégé. Renvoie les hachages SHA-256 de stdout/stderr au lieu des corps complets. Rejette fermement shell:true. |
register_allowlist | mutation | Enregistre un nom d'outil avec un exécutable + une regex d'arguments. Sans enregistrement, le refus par défaut s'applique. |
audit_source | lecture seule | Analyse un chemin TS/JS à la recherche d'anti-patterns d'injection de shell. Renvoie AuditFinding[] + un résumé. |
audit_report | lecture seule | Formate un résultat d'audit en markdown / json / SARIF 2.1.0. |
replay_check | lecture seule | Calcule le hachage SHA-256 canonique d'une invocation et indique s'il figure déjà dans la fenêtre de rejeu. |
sandbox_status | lecture seule | Indique le profil de bac à sable actif + les limites concrètes + l'indicateur d'activation cgroup-v2. |
trust_tier | lecture seule | Déduit le niveau LOW/MEDIUM/HIGH/CRITICAL d'un outil enregistré ainsi que des conseils d'amélioration. |
Profils de bac à sable
| Profil | Délai d'expiration | stdout max | stderr max | Budget de FD | cgroup-v2 |
|---|---|---|---|---|---|
strict | 5 s | 1 Mo | 256 Ko | 32 | oui (cpu/mémoire) |
standard (par défaut) | 30 s | 10 Mo | 1 Mo | 256 | oui |
permissive | 5 min | 100 Mo | 10 Mo | 1024 | non |
L'appelant peut resserrer via timeoutMs / fdBudget à chaque appel. L'appelant ne peut pas élargir
au-delà du profil.
Niveaux de confiance
| Niveau | Condition |
|---|---|
| LOW | outil non enregistré (refus par défaut) |
| MEDIUM | enregistré mais argsPatterns vide (tous les arguments autorisés) |
| HIGH | argsPatterns défini mais bac à sable ou traqueur de rejeu inactif |
| CRITICAL | argsPatterns + bac à sable + rejeu tous actifs |
Passez de LOW → CRITICAL en enregistrant l'outil + en définissant argsPatterns + en passant
par guardExec/guardSpawn (qui activent toujours le bac à sable + le rejeu).
Démarrage rapide de la bibliothèque
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
Audit CLI
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
Codes de sortie :
0propre (aucune détection au niveau plancher ou au-dessus)1détections présentes2erreurs d'analyse / d'E/S
Les ignoreGlobs personnalisés transmis à scanPath ou à l'outil MCP audit_source acceptent
au maximum 128 chaînes. Chaque chaîne est limitée à 1024 unités de code UTF-16 et à 16
caractères d'ouverture de groupe non échappés ((, {, [). Les entrées dépassant ces
budgets déclenchent une erreur contrôlée avant la mise en correspondance.
Bibliothèque d'anti-patterns (12 règles)
| ID | Sévérité | Déclenché par |
|---|---|---|
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 sans maxBuffer |
missing_timeout | LOW | exec/spawn sans timeout |
Le scanner résout les liaisons child_process renommées avant la mise en correspondance,
de sorte que les formes dangereuses ci-dessous sont détectées même lorsque l'appel passe par un
alias plutôt que par un littéral child_process.exec :
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(...)
Les variantes synchrones (spawnSync, execFileSync) partagent leurs règles asynchrones,
et shell_true_option se déclenche également sur un shell sous forme de chaîne ({ shell: "/bin/sh" })
ou sur une valeur de shell dynamique — pas seulement sur le littéral { shell: true }. Un
promisify d'une fonction non liée à child_process, une déstructuration depuis un autre
module, et { shell: false } restent propres (aucun faux positif).