MCP

MCP 安全最佳实践:保护你的 AI Agent 系统

全面解析 Model Context Protocol 的安全风险和防护策略,包括权限控制、输入验证、审计日志等关键安全实践。

MCP 赋予了 AI Agent 强大的外部操作能力——读写文件、执行查询、调用 API。但能力越大,风险越大。一个配置不当的 MCP Server 可能成为攻击者入侵系统的跳板。这篇文章将深入分析 MCP 的安全风险,并提供切实可行的防护策略。

MCP 的安全威胁模型

在讨论防护措施之前,我们需要理解 MCP 系统面临的主要威胁。

命令注入攻击

当 MCP Server 将 LLM 的输入直接传递给 shell 命令或 SQL 查询时,攻击者可能通过精心构造的提示词注入恶意命令。

// 危险:直接拼接用户输入
server.tool('run_command', {}, async ({ cmd }) => {
  const result = await exec(cmd);  // 如果 cmd 是 "ls; rm -rf /" 呢?
  return { content: [{ type: 'text', text: result }] };
});

权限提升

如果 MCP Server 以过高的系统权限运行,LLM 可能被诱导执行超出预期范围的操作。比如一个文件管理 Server 不应该有权访问 /etc/shadow

数据泄露

MCP Server 可能无意中将敏感数据暴露给 LLM,而 LLM 可能在后续对话中泄露这些信息。

间接提示注入

攻击者可以在外部数据源(如网页、文件)中嵌入恶意提示词,当 MCP Server 读取这些数据时,LLM 可能被劫持执行恶意操作。

防护策略

1. 最小权限原则

MCP Server 应该只拥有完成其功能所需的最小权限。

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import * as fs from 'fs/promises';
import * as path from 'path';

const ALLOWED_DIRS = ['/home/user/documents', '/home/user/projects'];

function isPathAllowed(filePath: string): boolean {
  const resolved = path.resolve(filePath);
  return ALLOWED_DIRS.some(dir => resolved.startsWith(dir));
}

server.tool(
  'read_file',
  '读取指定文件',
  { filePath: z.string() },
  async ({ filePath }) => {
    if (!isPathAllowed(filePath)) {
      return {
        content: [{ type: 'text', text: '权限拒绝:文件路径不在允许范围内' }],
        isError: true,
      };
    }
    const content = await fs.readFile(filePath, 'utf-8');
    return { content: [{ type: 'text', text: content }] };
  }
);

2. 输入验证与净化

所有从 LLM 接收的输入都必须经过严格验证。

import { z } from 'zod';

// 使用 Zod 进行严格验证
const QuerySchema = z.object({
  sql: z.string()
    .max(1000, '查询过长')
    .refine(sql => sql.trim().toUpperCase().startsWith('SELECT'), {
      message: '仅支持 SELECT 查询',
    })
    .refine(sql => !/(DROP|DELETE|UPDATE|INSERT|ALTER)/i.test(sql), {
      message: '不允许修改操作',
    }),
});

3. 命令白名单

对于需要执行系统命令的 Server,使用白名单而非黑名单。

const ALLOWED_COMMANDS = new Map([
  ['git', ['status', 'log', 'diff', 'branch']],
  ['npm', ['list', 'outdated', 'audit']],
]);

server.tool(
  'run_command',
  '执行允许的系统命令',
  {
    command: z.string(),
    args: z.array(z.string()),
  },
  async ({ command, args }) => {
    const allowed = ALLOWED_COMMANDS.get(command);
    if (!allowed || !allowed.includes(args[0])) {
      return {
        content: [{ type: 'text', text: `命令不允许:${command} ${args[0]}` }],
        isError: true,
      };
    }
    // 执行命令...
  }
);

4. 审计日志

记录所有工具调用,便于事后审查和异常检测。

import * as fs from 'fs';

const auditLog = fs.createWriteStream('audit.log', { flags: 'a' });

function logAudit(toolName: string, args: any, result: string, isError: boolean) {
  const entry = {
    timestamp: new Date().toISOString(),
    tool: toolName,
    args,
    resultLength: result.length,
    isError,
  };
  auditLog.write(JSON.stringify(entry) + '\n');
}

// 在工具调用时记录
server.tool('my_tool', schema, async (args) => {
  const result = await doWork(args);
  logAudit('my_tool', args, result.content[0].text, result.isError || false);
  return result;
});

5. 速率限制

防止 LLM 被诱导进行高频调用导致资源耗尽。

class RateLimiter {
  private calls: Map<string, number[]> = new Map();

  check(toolName: string, maxPerMinute: number): boolean {
    const now = Date.now();
    const window = 60 * 1000;
    const timestamps = this.calls.get(toolName) || [];
    const recent = timestamps.filter(t => now - t < window);

    if (recent.length >= maxPerMinute) return false;

    recent.push(now);
    this.calls.set(toolName, recent);
    return true;
  }
}

const limiter = new RateLimiter();

server.tool('search', schema, async (args) => {
  if (!limiter.check('search', 10)) {
    return {
      content: [{ type: 'text', text: '请求过于频繁,请稍后再试' }],
      isError: true,
    };
  };
  // 执行搜索...
});

6. 敏感数据过滤

在返回给 LLM 之前,过滤掉敏感信息。

const SENSITIVE_PATTERNS = [
  /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,  // 信用卡号
  /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,  // 邮箱
  /-----BEGIN.*KEY-----/g,  // 密钥
];

function sanitizeOutput(text: string): string {
  let sanitized = text;
  for (const pattern of SENSITIVE_PATTERNS) {
    sanitized = sanitized.replace(pattern, '[REDACTED]');
  }
  return sanitized;
}

部署安全

网络隔离

MCP Server 应该部署在独立的网络环境中,限制其对外部服务的访问范围。使用防火墙规则只允许必要的网络连接。

进程隔离

每个 MCP Server 应该以独立的低权限用户运行,使用容器或沙箱进行进程隔离。

FROM node:18-slim
RUN useradd -m mcpuser
USER mcpuser
WORKDIR /app
COPY --chown=mcpuser:mcpuser . .
CMD ["node", "server.js"]

密钥管理

不要在配置文件中硬编码密钥。使用环境变量或密钥管理服务。

{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "${GITHUB_TOKEN}"
      }
    }
  }
}

常见问题(FAQ)

MCP 协议本身有安全机制吗?

MCP 协议层不包含安全机制,安全控制由 Client 和 Server 实现负责。这是一个有意的设计决策——安全策略应该由应用层根据具体需求定制。

如何防止 LLM 被提示注入攻击?

结合输入验证、输出过滤和权限控制。对于从外部数据源读取的内容,使用明确的分隔符标记数据区域和指令区域,避免 LLM 混淆。

MCP Server 可以信任 LLM 的输入吗?

绝对不可以。所有 LLM 输入都应该被视为不可信的用户输入,经过完整的验证和净化处理。

总结

MCP 的安全不是协议层能自动解决的问题,需要开发者在每个环节主动实施防护。最小权限、输入验证、审计日志是三个最基本的防线。在此基础上,根据具体场景增加速率限制、敏感数据过滤和部署隔离等措施。

安全是一个持续的过程,不是一次性的配置。定期审查 MCP Server 的权限配置、审计日志和安全策略,确保它们与当前的威胁模型保持同步。