Комплексная защита для MCP stdio серверов: готовые обёртки guardExec/guardSpawn, CLI для аудита AST, эталонный MCP сервер. Закрывает класс уязвимостей stdio-RCE в 200k серверах от Ox-Security (LiteLLM CVE-2025-69256). MIT, TypeScript, Node >= 20.
Часть StudioMeyer MCP Stack — Создано на Майорке 🌴 · ⭐ если вы это используете
с помощью allowlist + sandbox + обнаружения повторов, а также включает CLI-аудит на основе AST (mcp-shellguard-audit),
который сканирует исходники MCP-серверов на наличие несанитизированных вызовов оболочки. Закрывает класс уязвимостей
MCP stdio-RCE, описанный Ox-Security (200 тыс. уязвимых серверов, раскрытие в мае 2026 г.).
@modelcontextprotocol/sdk ^1.29.0npm install mcp-stdio-shellguard
Или запустите CLI аудита напрямую без установки:
npx -y -p mcp-stdio-shellguard mcp-shellguard-audit scan ./src
Три уровня, подключаемые по частям:
guardExec / guardSpawn, которые вы вызываете из
собственного MCP-сервера. Allowlist с запретом по умолчанию, профили песочницы, окно повтора.mcp-shellguard-audit scan <path> обходит AST и сообщает
о 12 антипаттернах от LOW (no timeout) до CRITICAL (exec(\...${userInput}...`)`).mcp-stdio-shellguard-demo предоставляет 8 инструментов,
чтобы MCP Inspector / Claude Desktop могли напрямую управлять набором.| Инструмент | Тип | Назначение |
|---|---|---|
guard_exec | destructive | Защищённый child_process.exec. Принудительно использует вектор args[], allowlist + песочницу + повтор. Возвращает stdout, stderr, exitCode, canonicalHash, isReplay, trustTier. |
guard_spawn | destructive | Защищённый child_process.spawn. Возвращает SHA-256-хэши stdout/stderr вместо полных тел. Жёстко отклоняет shell:true. |
register_allowlist | mutating | Регистрирует имя инструмента с исполняемым файлом + regex аргументов. Без регистрации действует запрет по умолчанию. |
audit_source | read-only | Сканирует путь TS/JS на антипаттерны shell-инъекций. Возвращает AuditFinding[] + сводку. |
audit_report | read-only | Форматирует результат аудита как markdown / json / SARIF 2.1.0. |
replay_check | read-only | Вычисляет канонический SHA-256-хэш для вызова и сообщает, находится ли он уже в окне повтора. |
sandbox_status | read-only | Сообщает активный профиль песочницы + конкретные лимиты + флаг активности cgroup-v2. |
trust_tier | read-only | Выводит уровень LOW/MEDIUM/HIGH/CRITICAL для зарегистрированного инструмента плюс подсказки по улучшению. |
| Профиль | Таймаут | Макс. stdout | Макс. stderr | Бюджет FD | cgroup-v2 |
|---|---|---|---|---|---|
strict | 5 с | 1 МБ | 256 КБ | 32 | да (cpu/memory) |
standard (по умолчанию) | 30 с | 10 МБ | 1 МБ | 256 | да |
permissive | 5 мин | 100 МБ | 10 МБ | 1024 | нет |
Вызывающая сторона может ужесточить через timeoutMs / fdBudget для каждого вызова. Вызывающая сторона не может расширить
за пределы профиля.
| Уровень | Условие |
|---|---|
| LOW | инструмент не зарегистрирован (запрет по умолчанию) |
| MEDIUM | зарегистрирован, но argsPatterns пуст (разрешены любые аргументы) |
| HIGH | argsPatterns задан, но песочница или трекер повторов неактивны |
| CRITICAL | argsPatterns + песочница + повтор все активны |
Повышение LOW → CRITICAL достигается регистрацией инструмента + заданием argsPatterns + запуском
через guardExec/guardSpawn (которые всегда активируют песочницу + повтор).
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
Коды выхода:
0 — чисто (нет находок на уровне порога или выше)1 — есть находки2 — ошибки разбора / ввода-выводаПользовательские ignoreGlobs, передаваемые в scanPath или в MCP-инструмент audit_source, принимают не более 128 строк. Каждая строка ограничена 1024 кодовыми единицами UTF-16 и 16 неэкранированными открывающими группирующими символами ((, {, [). Входные данные, превышающие эти лимиты, вызывают контролируемую ошибку до начала сопоставления.
| ID | Уровень серьёзности | Срабатывает на |
|---|---|---|
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 без maxBuffer |
missing_timeout | LOW | exec/spawn без timeout |
Сканер разрешает переименованные привязки child_process перед сопоставлением,
поэтому опасные конструкции ниже перехватываются даже тогда, когда вызов идёт через
псевдоним, а не через литеральный 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(...)Синхронные варианты (spawnSync, execFileSync) используют те же правила, что и их
асинхронные аналоги, а shell_true_option также срабатывает на строковом shell ({ shell: "/bin/sh" })
или динамическом значении shell — не только на литерале { shell: true }.
promisify от функции, не относящейся к child_process, деструктуризация из другого
модуля и { shell: false } остаются чистыми (без ложных срабатываний).
// shellguard:ignore-next-line — подавить одну находку// shellguard:ignore-file — подавить весь файл (редко; предпочтительнее построчно)