
MCP stdio 서버용 심층 방어 번들: 드롭인 guardExec/guardSpawn 래퍼, AST 감사 CLI, 레퍼런스 MCP 서버. Ox-Security 200k 서버 stdio-RCE 클래스(LiteLLM CVE-2025-69256)를 차단합니다. MIT, TypeScript, Node >= 20.
StudioMeyer MCP Stack의 일부 — 마요르카에서 제작 🌴 · 사용한다면 ⭐
@modelcontextprotocol/sdk ^1.29.0npm install mcp-stdio-shellguard
또는 설치 없이 감사 CLI를 직접 실행:
npx -y -p mcp-stdio-shellguard mcp-shellguard-audit scan ./src
세 가지 계층으로, 부분적으로 선택 적용(opt-in)할 수 있습니다:
guardExec / guardSpawn. 기본 거부(default-deny) 허용 목록, 샌드박스 프로파일, 재생 창(replay window).mcp-shellguard-audit scan <path>가 AST를 탐색하여 LOW(no timeout)부터 CRITICAL(exec(\...${userInput}...`)`)까지 12가지 안티패턴을 보고합니다.mcp-stdio-shellguard-demo는 8개의 도구를 노출하여 MCP Inspector / Claude Desktop이 번들을 직접 구동할 수 있게 합니다.호출자는 timeoutMs / fdBudget로 호출별 제한을 더 강화할 수 있습니다. 호출자는 프로파일을 넘어 제한을 완화할 수 없습니다.
| 등급 | 조건 |
|---|---|
| LOW | 도구가 등록되지 않음 (기본 거부) |
| MEDIUM | 등록되었지만 argsPatterns가 비어 있음 (모든 인자 허용) |
| HIGH | argsPatterns가 설정되었지만 샌드박스 또는 재생 추적기가 비활성 |
도구 등록 + argsPatterns 설정 + guardExec/guardSpawn을 통한 실행(항상 sandbox + replay를 활성화함)으로 LOW → CRITICAL로 끌어올릴 수 있습니다.
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 파싱 / IO 오류스캐너는 매칭 전에 이름이 바뀐 child_process 바인딩을 해석하므로, 아래의 위험한 형태는 호출이 리터럴 child_process.exec가 아닌 별칭(alias)을 통해 이루어져도 감지됩니다:
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: true }뿐만 아니라 문자열 셸({ shell: "/bin/sh" }) 또는 동적 셸 값에서도 발생합니다. child_process가 아닌 함수의 promisify, 다른 모듈에서의 구조 분해, { shell: false }는 깨끗하게 유지됩니다(오탐 없음).
// shellguard:ignore-next-line — 발견 사항 1건을 억제합니다// shellguard:ignore-file — 전체 파일을 억제합니다 (드묾; 줄 단위 권장)Ox-Security는 (2026-05) 20만 개 이상의 MCP stdio 서버가 LLM 도구 인자에서 직접 가져온 사용자 입력을 담은 템플릿 리터럴로 child_process.exec를 래핑한다고 공개했습니다. LiteLLM v1.83.6이 대표적인 사례였습니다(CVE는 1.83.7에서 패치됨). 이 번들은 방어 보안(defensive-security) 대응책입니다: 해당 클래스를 차단하는 드롭인 가드 + 스캐너. AWS Linux seccomp + Chromium 샌드박스 등급에서 영감을 받았습니다.
HOOK_RECIPES.md — 위험한 도구 호출을 자동 차단하는 Claude Code 훅 레시피CHANGELOG.md — 릴리스 기록MIT — Copyright (c) 2026 Matthias Meyer (StudioMeyer)
| 도구 | 유형 | 용도 |
|---|
guard_exec | 파괴적 | 방어된 child_process.exec. args[] 벡터를 강제하고 허용 목록 + 샌드박스 + 재생 감지를 적용합니다. stdout, stderr, exitCode, canonicalHash, isReplay, trustTier를 반환합니다. |
guard_spawn | 파괴적 | 방어된 child_process.spawn. 전체 본문 대신 stdout/stderr의 SHA-256 해시를 반환합니다. shell:true를 강하게 거부합니다. |
register_allowlist | 변경형 | 실행 파일 + 인자 regex로 도구 이름을 등록합니다. 등록하지 않으면 기본 거부(default-deny)가 적용됩니다. |
audit_source | 읽기 전용 | TS/JS 경로에서 셸 인젝션 안티패턴을 스캔합니다. AuditFinding[] + 요약을 반환합니다. |
audit_report | 읽기 전용 | 감사 결과를 markdown / json / SARIF 2.1.0 형식으로 포맷합니다. |
replay_check | 읽기 전용 | 호출에 대한 표준 SHA-256 해시를 계산하고 재생 창에 이미 있는지 보고합니다. |
sandbox_status | 읽기 전용 | 활성 샌드박스 프로파일 + 구체적인 제한 + cgroup-v2 활성 플래그를 보고합니다. |
trust_tier | 읽기 전용 | 등록된 도구에 대한 LOW/MEDIUM/HIGH/CRITICAL 등급을 도출하고 개선 힌트를 제공합니다. |
| 프로파일 | 타임아웃 | 최대 stdout | 최대 stderr | FD 예산 | cgroup-v2 |
|---|
strict | 5초 | 1MB | 256KB | 32 | 예 (cpu/memory) |
standard (기본값) | 30초 | 10MB | 1MB | 256 | 예 |
permissive | 5분 | 100MB | 10MB | 1024 | 아니요 |
| CRITICAL | argsPatterns + sandbox + replay 모두 활성 |
| 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 | maxBuffer 없는 exec |
missing_timeout | LOW | timeout 없는 exec/spawn |