feat(mcp-py): 代码沙箱落地 —— AST 静态守卫 + Docker 隔离执行(弃用桩)

mcp-py 的 run_code/secure_sandbox 此前全是桩。落地两层防御:

1) 静态守卫 sandbox.SecureSandbox.static_guard(纯 AST,执行前第一道)
   - 拦危险导入(os/sys/subprocess/socket/ctypes/pickle/requests…)、危险调用
     (eval/exec/compile/__import__/open…)、逃逸属性(__subclasses__/__globals__…)、语法错误。
   - 返回 (放行, 原因)。

2) 隔离执行 interpreter.CodeInterpreter.execute(Docker,真隔离)
   - network_disabled 禁网;user=65534 非 root + cap_drop=ALL + no-new-privileges;
     read_only 根 + /tmp tmpfs;mem/memswap(禁swap)/nano_cpus/pids_limit 限资源;
     python -I 隔离模式;wait 超时即 kill;容器一次性 remove。
   - 无 Docker SDK/daemon 时 available()=False 优雅降级,不阻断服务。

gateway:run_code(标准档 256m/0.5cpu/10s) 与 secure_sandbox(紧档 128m/5s) 均走
守卫→隔离,结果整理为 stdout/stderr/exit 可读文本。pyproject 启用 docker 依赖。

验证:
- 守卫 6 单测(放行安全码 / 拦危险导入·调用·逃逸属性 / 语法错误)全过。
- 隔离 4 项实跑(真 Docker):sum(range(10))→45 exit0;非root uid=65534;
  禁网 urlopen 失败(DNS解析错);while True 超时 3s 被 kill。
- 无 Docker 降级测过。

生产加固:可把执行运行时换 gVisor(runsc)/Kata(已在注释/PROGRESS 标注)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-18 11:26:08 +08:00
parent 9657a07bb5
commit cad5b14382
6 changed files with 245 additions and 25 deletions
@@ -93,9 +93,14 @@ class McpGateway:
return str(args.get("text", ""))
async def _run_code(self, args: dict) -> str:
"""静态守卫 → Docker 隔离执行(标准档 256m/0.5cpu/10s)。"""
code = str(args.get("code", ""))
result = await self.interpreter.execute(code) # Docker 隔离沙箱(桩)
return f"[run_code] Docker 隔离执行(桩) stdout={result.get('stdout','')!r}"
if not code.strip():
return "(空代码)"
ok, reason = self.sandbox.static_guard(code)
if not ok:
return f"🚫 静态守卫拒绝:{reason}"
return _fmt_result(await self.interpreter.execute(code))
async def _parse_document(self, args: dict) -> str:
"""文件 → 纯文本。content_b64=文件内容(base64)filename 决定解析器。"""
@@ -112,10 +117,28 @@ class McpGateway:
return await asyncio.to_thread(parsers.parse, filename, data)
async def _secure_sandbox(self, args: dict) -> str:
"""同 run_code 的守卫 + 隔离,更紧资源档(128m/5s),用于高风险代码。"""
code = str(args.get("code", ""))
out = await self.sandbox.run(code) # gVisor/KataVM(桩,含静态守卫)
return f"[secure_sandbox] gVisor 隔离执行(桩) out={out!r}"
if not code.strip():
return "(空代码)"
ok, reason = self.sandbox.static_guard(code)
if not ok:
return f"🚫 静态守卫拒绝:{reason}"
return "[secure] " + _fmt_result(await self.interpreter.execute(code, mem="128m", timeout=5))
async def close(self) -> None:
if self._nc is not None:
await self._nc.drain()
def _fmt_result(r: dict) -> str:
"""把执行结果 {ok,stdout,stderr,exit,degraded} 整理成可读文本。"""
if r.get("degraded"):
return f"⚠️ {r.get('stderr', 'Docker 不可用')}"
parts = []
if r.get("stdout", "").strip():
parts.append("stdout:\n" + r["stdout"].strip())
if r.get("stderr", "").strip():
parts.append("stderr:\n" + r["stderr"].strip())
parts.append(f"exit={r.get('exit')}")
return "\n".join(parts)