如何保护 MCP 服务器?
Source: Dev.to
从十余年的 API 开发经验中,我已经内化了一条关键原则:安全必须是基础,而不是事后补丁。对于管理文件、数据和关键资产的 MCP 服务器而言,多层防御是必不可少的。然而,加强安全并不等同于增加复杂度。本文概述了四项关键安全防护的实现方式:
- 严格的输入验证以阻止恶意数据
- 强大的身份验证以确认用户身份
- 精确的授权以定义访问权限
- 智能的资源限制以防止滥用
完成后,您的服务器将准备好投入生产环境。
MCP 服务器安全的核心原则
在深入代码之前,让我们先确定防御深度策略。
1. 输入验证
原则:对所有进入的数据保持怀疑。对每一条信息进行严格的验证、清理和核实。
理由:参数验证不足会打开关键漏洞的大门,例如目录遍历攻击(如访问 ../../etc/passwd)、代码注入或服务器不稳定。
2. 身份验证
原则:确认每个与服务器交互的实体的身份。每个传入请求都必须关联到一个已确认且已验证的身份。
理由:缺乏身份验证会让您的工具完全暴露于未授权访问,就像一扇未上锁的前门招来不速之客。
3. 授权
原则:验证具体的访问权限。即使身份验证成功,用户也只能执行与其角色相符的操作。
理由:比如实习生不应访问人力资源记录。细粒度的权限控制是保护敏感数据的关键。
4. 资源限制
原则:设定明确的配额、大小上限并实现连接超时。
理由:这些措施对于防止单个恶意行为者或意外错误以极高请求频率(如每秒 10 000 次)压垮服务器至关重要。
强大的输入验证
我们从最关键的防线——验证所有进入的数据——开始。通过创建 src/security/validator.ts 并实现如下代码来完成此任务:
// src/security/validator.ts
import path from 'path';
import { InputSchema } from '../mcp/protocol';
/**
* Validation error
*/
export class ValidationError extends Error {
constructor(
message: string,
public field?: string,
public expected?: string
) {
super(message);
this.name = 'ValidationError';
}
}
/**
* Parameter validator based on JSON Schema
*/
export class ParameterValidator {
/**
* Validate parameters according to schema
*/
static validate(params: any, schema: InputSchema): void {
// Check that params is an object
if (typeof params !== 'object' || params === null) {
throw new ValidationError('Parameters must be an object');
}
// Check required fields
for (const requiredField of schema.required) {
if (!(requiredField in params)) {
throw new ValidationError(
`Field '${requiredField}' is required`,
requiredField
);
}
}
// Validate each property
for (const [fieldName, fieldValue] of Object.entries(params)) {
const fieldSchema = schema.properties[fieldName];
if (!fieldSchema) {
throw new ValidationError(
`Field '${fieldName}' is not allowed`,
fieldName
);
}
this.validateField(fieldName, fieldValue, fieldSchema);
}
}
/**
* Validate a specific field
*/
private static validateField(
fieldName: string,
value: any,
schema: any
): void {
// Type validation
const actualType = typeof value;
const expectedType = schema.type;
if (expectedType === 'string' && actualType !== 'string') {
throw new ValidationError(
`Field '${fieldName}' must be a string`,
fieldName,
expectedType
);
}
if (expectedType === 'number' && actualType !== 'number') {
throw new ValidationError(
`Field '${fieldName}' must be a number`,
fieldName,
expectedType
);
}
if (expectedType === 'boolean' && actualType !== 'boolean') {
throw new ValidationError(
`Field '${fieldName}' must be a boolean`,
fieldName,
expectedType
);
}
// Enumeration validation
if (schema.enum && !schema.enum.includes(value)) {
throw new ValidationError(
`Field '${fieldName}' must be one of: ${schema.enum.join(', ')}`,
fieldName
);
}
// Length validation for strings
if (expectedType === 'string') {
if (schema.minLength && value.length < schema.minLength) {
throw new ValidationError(
`Field '${fieldName}' must be at least ${schema.minLength} characters`,
fieldName
);
}
if (schema.maxLength && value.length > schema.maxLength) {
throw new ValidationError(
`Field '${fieldName}' cannot exceed ${schema.maxLength} characters`,
fieldName
);
}
}
// Range validation for numbers
if (expectedType === 'number') {
if (schema.minimum !== undefined && value < schema.minimum) {
throw new ValidationError(
`Field '${fieldName}' cannot be less than ${schema.minimum}`,
fieldName
);
}
if (schema.maximum !== undefined && value > schema.maximum) {
throw new ValidationError(
`Field '${fieldName}' cannot exceed ${schema.maximum}`,
fieldName
);
}
}
// Pattern validation for strings
if (expectedType === 'string' && schema.pattern) {
const regex = new RegExp(schema.pattern);
if (!regex.test(value)) {
throw new ValidationError(
`Field '${fieldName}' doesn't match expected format`,
fieldName
);
}
}
}
}
/**
* File path validator
*/
export class PathValidator {
private allowedDirectories: string[];
private blockedPaths: string[];
constructor(allowedDirectories: string[], blockedPaths: string[] = []) {
// Resolve all paths to absolute
this.allowedDirectories = allowedDirectories.map(dir => path.resolve(dir));
this.blockedPaths = blockedPaths.map(p => path.resolve(p));
}
/**
* Validate that a path is safe
*/
validatePath(filePath: string): string {
// Resolve absolute path
const absolutePath = path.resolve(filePath);
// Check path traversal (../)
if (absolutePath.includes('..')) {
throw new ValidationError(
'Paths with ".." are not allowed (path traversal)'
);
}
// Check that path is in an allowed directory
const isInAllowedDir = this.allowedDirectories.some(dir =>
absolutePath.startsWith(dir)
);
if (!isInAllowedDir) {
throw new ValidationError('Path is outside allowed directories');
}
// Optional: block explicitly blacklisted paths
if (this.blockedPaths.includes(absolutePath)) {
throw new ValidationError('Path is explicitly blocked');
}
return absolutePath;
}
}