MCP协议 进阶 MCP 安全 权限控制 沙箱

MCP Server权限与安全:构建可信的执行环境

AIEng Hub
阅读约 17 分钟

引言

MCP Server是AI模型访问外部系统的桥梁,其安全性至关重要。一个不安全的MCP Server可能导致数据泄露、系统被控甚至更严重的后果。本文将系统讲解MCP Server的安全设计。

安全威胁模型

主要威胁类别

┌─────────────────────────────────────────────────────────────┐
│                   MCP Server 威胁模型                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  威胁类型               攻击向量              影响          │
│  ────────              ────────             ──────         │
│                                                             │
│  路径穿越          ../../../etc/passwd      文件泄露        │
│  命令注入          ; rm -rf /              系统破坏        │
│  SSRF             file:///etc/passwd       内网探测        │
│  权限提升          未授权敏感操作           数据泄露        │
│  拒绝服务          超大文件读取             资源耗尽        │
│  API密钥泄漏       日志/错误信息暴露        凭证泄漏        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
威胁风险等级典型场景
路径穿越LLM请求读取系统配置文件
命令注入通过参数执行系统命令
SSRF访问内网服务
权限提升越权访问其他用户的文件
DoS请求处理大文件导致OOM

输入验证与净化

严格参数验证

import { z } from "zod";
import path from "path";

// 文件路径验证
const ReadFileSchema = z.object({
  path: z.string()
    .min(1, "路径不能为空")
    .max(512, "路径过长")
    .refine(
      (val) => !val.includes(".."),
      "路径不能包含 '..'"
    )
    .refine(
      (val) => path.isAbsolute(val),
      "仅支持绝对路径"
    ),
});

// 命令参数验证
const ExecSchema = z.object({
  command: z.string()
    .min(1)
    .max(256)
    .refine(
      (val) => /^[a-zA-Z0-9\s\-_\.\/]+$/.test(val),
      "命令包含非法字符"
    ),
  args: z.array(z.string()).max(10).default([]),
});

// URL验证 - 防止SSRF
const FetchSchema = z.object({
  url: z.string()
    .url("必须是合法URL")
    .refine(
      (val) => {
        try {
          const parsed = new URL(val);
          // 禁止内网地址
          const blockedHosts = [
            "localhost", "127.0.0.1", "0.0.0.0",
            "10.", "172.16.", "172.17.", "172.18.",
            "172.19.", "172.20.", "172.21.", "172.22.",
            "172.23.", "172.24.", "172.25.", "172.26.",
            "172.27.", "172.28.", "172.29.", "172.30.",
            "172.31.", "192.168.", "169.254.",
          ];
          return !blockedHosts.some(h => 
            parsed.hostname.startsWith(h) ||
            parsed.hostname === h
          );
        } catch {
          return false;
        }
      },
      "禁止访问内网地址"
    ),
});

路径安全控制

基于白名单的路径控制

class PathGuard {
  private allowedBases: string[];
  private blockList: string[];

  constructor(allowedDirs: string[]) {
    this.allowedBases = allowedDirs.map(d => path.resolve(d));
    this.blockList = [
      "/etc",
      "/proc",
      "/sys",
      "/dev",
      "/boot",
      "/var/log",
      "/var/lib",
      "/root",
    ];
  }

  validate(requestPath: string): string {
    const resolved = path.resolve(requestPath);

    // 检查是否在黑名单中
    for (const blocked of this.blockList) {
      if (resolved.startsWith(blocked)) {
        throw new Error(`禁止访问系统目录: ${blocked}`);
      }
    }

    // 检查是否在白名单中
    const isAllowed = this.allowedBases.some(base =>
      resolved.startsWith(base)
    );

    if (!isAllowed) {
      throw new Error(
        `路径不在允许范围内。允许的根目录: ${this.allowedBases.join(", ")}`
      );
    }

    // 检查符号链接(防止绕过)
    const realPath = fs.realpathSync(resolved);
    const isRealAllowed = this.allowedBases.some(base =>
      realPath.startsWith(base)
    );

    if (!isRealAllowed) {
      throw new Error("路径解析后不在允许范围内(符号链接绕过检测)");
    }

    return resolved;
  }
}

// 使用示例
const pathGuard = new PathGuard([
  "/home/user/projects",
  "/home/user/data",
]);

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "read_file") {
    const safePath = pathGuard.validate(request.params.arguments.path);
    // 安全路径,可以继续处理
  }
});

沙箱执行环境

进程隔离

import { execSync, spawn } from "child_process";

class SandboxExecutor {
  async executeInSandbox(
    command: string,
    args: string[],
    timeout: number = 30000
  ): Promise<string> {
    // 设置严格的环境变量白名单
    const allowedEnv: Record<string, string> = {
      PATH: "/usr/local/bin:/usr/bin:/bin",
      HOME: "/tmp/sandbox",
    };

    return new Promise((resolve, reject) => {
      const child = spawn(command, args, {
        env: allowedEnv,
        cwd: "/tmp/sandbox",
        timeout,
        uid: 1001,       // 低权限用户
        gid: 1001,
        stdio: ["pipe", "pipe", "pipe"],
      });

      let stdout = "";
      let stderr = "";

      child.stdout.on("data", (data) => { stdout += data; });
      child.stderr.on("data", (data) => { stderr += data; });

      child.on("close", (code) => {
        if (code !== 0) {
          reject(new Error(`沙箱执行失败 (${code}): ${stderr}`));
        } else {
          resolve(stdout);
        }
      });

      child.on("error", reject);

      // 超时处理
      setTimeout(() => {
        child.kill("SIGKILL");
        reject(new Error("沙箱执行超时"));
      }, timeout);
    });
  }
}

容器沙箱(Docker)

class DockerSandbox {
  async runInContainer(
    image: string,
    command: string[],
    workDir: string,
    timeout: number = 30000
  ): Promise<string> {
    const execSync = require("child_process").execSync;

    const dockerArgs = [
      "docker", "run", "--rm",
      "--network", "none",           // 无网络
      "--read-only",                 // 只读文件系统
      "--memory", "512m",            // 内存限制
      "--cpus", "1",                 // CPU限制
      "--pids-limit", "50",          // 进程数限制
      "--security-opt", "no-new-privileges:true",
      "--cap-drop", "ALL",           // 删除所有能力
      "-v", `${workDir}:/data:ro`,   // 只读挂载
      image,
      ...command,
    ];

    try {
      const output = execSync(dockerArgs.join(" "), { timeout });
      return output.toString();
    } catch (error) {
      throw new Error(`容器执行失败: ${error.message}`);
    }
  }
}

API密钥管理

安全存储与访问

// 环境变量管理
class SecureConfig {
  private static instance: SecureConfig;
  private secrets: Map<string, string> = new Map();

  static getInstance(): SecureConfig {
    if (!SecureConfig.instance) {
      SecureConfig.instance = new SecureConfig();
    }
    return SecureConfig.instance;
  }

  loadFromEnv(prefix: string = "MCP_"): void {
    for (const [key, value] of Object.entries(process.env)) {
      if (key.startsWith(prefix) && value) {
        this.secrets.set(key, value);
      }
    }
  }

  getSecret(key: string): string | undefined {
    return this.secrets.get(key);
  }

  // 工具函数:在响应中过滤敏感信息
  sanitizeOutput(text: string): string {
    for (const [, value] of this.secrets) {
      text = text.replace(new RegExp(value, "g"), "***");
    }
    return text;
  }
}

// 使用
const config = SecureConfig.getInstance();
config.loadFromEnv();

最小权限原则

interface ToolPermission {
  toolName: string;
  requiredPermissions: string[];
  rateLimit: number;
  allowedIPs?: string[];
}

class PermissionManager {
  private permissions: Map<string, ToolPermission> = new Map();

  registerPermission(tool: ToolPermission): void {
    this.permissions.set(tool.toolName, tool);
  }

  checkPermission(
    toolName: string,
    context: { userId?: string; ip?: string }
  ): boolean {
    const perm = this.permissions.get(toolName);
    if (!perm) return false;

    // 检查IP(如果设置)
    if (perm.allowedIPs && context.ip) {
      if (!perm.allowedIPs.includes(context.ip)) {
        return false;
      }
    }

    return true;
  }
}

// 注册工具权限
const permMgr = new PermissionManager();
permMgr.registerPermission({
  toolName: "delete_file",
  requiredPermissions: ["file:write"],
  rateLimit: 10,
});
permMgr.registerPermission({
  toolName: "read_file",
  requiredPermissions: ["file:read"],
  rateLimit: 100,
});

日志安全

安全的日志记录

class SafeLogger {
  private sensitivePatterns: RegExp[] = [
    /api[_-]?key[=:]\s*['"]?[^'"&\s]+/gi,
    /secret[=:]\s*['"]?[^'"&\s]+/gi,
    /password[=:]\s*['"]?[^'"&\s]+/gi,
    /bearer\s+[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+/gi,
    /Authorization:\s*Bearer\s+\S+/gi,
  ];

  sanitize(message: string): string {
    let safe = message;
    for (const pattern of this.sensitivePatterns) {
      safe = safe.replace(pattern, "[SANITIZED]");
    }
    return safe;
  }

  info(message: string, data?: any): void {
    const safeMsg = this.sanitize(message);
    const safeData = data ? this.sanitize(JSON.stringify(data)) : "";
    console.error(`[INFO] ${safeMsg} ${safeData}`);
  }

  error(message: string, error?: any): void {
    const safeMsg = this.sanitize(message);
    console.error(`[ERROR] ${safeMsg}`, error);
  }
}

安全清单

MCP Server安全审查清单

项目检查点状态
输入验证所有参数使用JSON Schema/Zod验证
路径安全路径白名单 + 符号链接检测
命令注入不使用eval/exec拼接命令
SSRF防护URL白名单/内网地址过滤
沙箱隔离容器或子进程隔离
资源限制超时、内存、文件大小限制
密钥管理环境变量 + 响应过滤
日志安全敏感信息脱敏
最小权限工具级权限控制
审计日志所有调用记录

总结

MCP Server安全的核心原则:

安全原则                         实现方式
──────────────────────────────────────────────────
最小权限          →  只给工具所需的最小访问范围
深度防御          →  输入验证 + 路径检查 + 沙箱
默认拒绝          →  白名单模式,未明确允许的禁止
不信任输入        →  所有参数严格校验
可审计            →  每次调用记录 + 敏感信息脱敏

下一步学习建议:


本文最后更新于 2024-07-08。