如何保护 MCP 服务器?

发布: (2025年12月11日 GMT+8 13:36)
6 min read
原文: Dev.to

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;
  }
}
Back to Blog

相关文章

阅读更多 »