引言
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。