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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user