cad5b14382
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>
99 lines
4.0 KiB
Python
99 lines
4.0 KiB
Python
"""Docker 隔离沙箱:Code Interpreter 在一次性容器里执行不可信代码。
|
||
|
||
隔离要点(安全默认):
|
||
- network_disabled:禁网(无外联 / 不能回连)。
|
||
- user=65534(nobody) + cap_drop=ALL + no-new-privileges:非 root、丢全部能力、禁提权。
|
||
- read_only 根文件系统 + 仅 /tmp 给小块 tmpfs:不能写镜像、不留痕。
|
||
- mem_limit + memswap=mem(禁 swap)+ nano_cpus + pids_limit:限内存/CPU/进程数。
|
||
- python -I(隔离模式,忽略环境与用户 site)+ wait 超时即 kill + 一次性 remove。
|
||
Docker 不可用时优雅降级(available()=False),不阻断服务。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import logging
|
||
|
||
log = logging.getLogger("mcp_py")
|
||
|
||
|
||
class CodeInterpreter:
|
||
"""Docker 隔离沙箱 · Code Interpreter。"""
|
||
|
||
def __init__(self) -> None:
|
||
self._client = None
|
||
self._init_err = "未初始化"
|
||
try:
|
||
import docker # 延迟导入:未装 docker SDK 时不影响其它工具
|
||
|
||
self._client = docker.from_env()
|
||
self._client.ping()
|
||
self._init_err = ""
|
||
log.info("[interpreter] Docker 就绪")
|
||
except Exception as e: # noqa: BLE001
|
||
self._init_err = str(e)
|
||
log.warning("[interpreter] Docker 不可用,代码执行降级: %s", e)
|
||
|
||
def available(self) -> bool:
|
||
return self._client is not None
|
||
|
||
async def execute(
|
||
self,
|
||
code: str,
|
||
*,
|
||
image: str = "python:3.11-slim",
|
||
mem: str = "256m",
|
||
cpu: float = 0.5,
|
||
timeout: int = 10,
|
||
pids: int = 64,
|
||
) -> dict:
|
||
"""在一次性隔离容器中执行代码,返回 {ok,stdout,stderr,exit,degraded}。"""
|
||
if self._client is None:
|
||
return {"ok": False, "stdout": "", "stderr": f"Docker 不可用:{self._init_err}", "exit": -1, "degraded": True}
|
||
# Docker SDK 为阻塞式,丢线程池避免卡事件循环。
|
||
return await asyncio.to_thread(self._run_blocking, code, image, mem, cpu, timeout, pids)
|
||
|
||
def _run_blocking(self, code: str, image: str, mem: str, cpu: float, timeout: int, pids: int) -> dict:
|
||
container = None
|
||
try:
|
||
container = self._client.containers.run(
|
||
image,
|
||
["python", "-I", "-c", code],
|
||
detach=True,
|
||
network_disabled=True,
|
||
mem_limit=mem,
|
||
memswap_limit=mem, # = mem → 禁用 swap
|
||
nano_cpus=int(cpu * 1e9),
|
||
pids_limit=pids,
|
||
read_only=True,
|
||
tmpfs={"/tmp": "size=16m"},
|
||
cap_drop=["ALL"],
|
||
security_opt=["no-new-privileges"],
|
||
user="65534:65534", # nobody
|
||
stdin_open=False,
|
||
tty=False,
|
||
)
|
||
timed_out = False
|
||
try:
|
||
res = container.wait(timeout=timeout)
|
||
exit_code = int(res.get("StatusCode", -1))
|
||
except Exception: # noqa: BLE001 容器 wait 超时(requests ReadTimeout)
|
||
try:
|
||
container.kill()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
exit_code, timed_out = -1, True
|
||
stdout = container.logs(stdout=True, stderr=False).decode("utf-8", "replace")
|
||
stderr = container.logs(stdout=False, stderr=True).decode("utf-8", "replace")
|
||
if timed_out:
|
||
stderr = (stderr + f"\n[超时 {timeout}s,已终止]").strip()
|
||
return {"ok": exit_code == 0 and not timed_out, "stdout": stdout, "stderr": stderr, "exit": exit_code, "degraded": False}
|
||
except Exception as e: # noqa: BLE001
|
||
return {"ok": False, "stdout": "", "stderr": f"执行失败:{e}", "exit": -1, "degraded": False}
|
||
finally:
|
||
if container is not None:
|
||
try:
|
||
container.remove(force=True)
|
||
except Exception: # noqa: BLE001
|
||
pass
|