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 的权限配置、审计日志和安全策略,确保它们与当前的威胁模型保持同步。