mcp-server-security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMCP Server Security
MCP 服务器安全
Comprehensive hardening guide for Model Context Protocol (MCP) servers. MCP is the open
standard for connecting AI agents to external tools, data sources, and services. Because
MCP servers execute real actions on real infrastructure, they are a high-value attack
surface. This skill covers every layer of defense you need before exposing an MCP server
to agents in production.
这是一份针对Model Context Protocol (MCP)服务器的全面加固指南。MCP是用于连接AI Agent与外部工具、数据源及服务的开放标准。由于MCP服务器会在真实基础设施上执行实际操作,因此它们属于高价值攻击面。本指南涵盖了将MCP服务器部署到生产环境供Agent使用前所需的所有防御层面。
1. When to Use This Skill
1. 何时使用本指南
Apply this skill whenever you are:
- Deploying an MCP server that exposes tools (filesystem, database, API) to AI agents.
- Connecting an agent runtime (Claude Desktop, Cursor, a custom orchestrator) to one or more MCP servers over stdio, SSE, or Streamable HTTP transport.
- Building a multi-tenant platform where multiple users share the same MCP server.
- Passing sensitive data (PII, credentials, internal documents) through MCP resources.
- Operating in a regulated environment (SOC 2, HIPAA, PCI-DSS) where tool invocations must be auditable.
If your MCP server only runs locally over stdio for a single developer with no network
exposure, you can relax some transport-layer controls -- but input validation and audit
logging still apply.
在以下场景中应用本指南:
- 部署向AI Agent开放工具(文件系统、数据库、API)的MCP服务器时。
- 通过stdio、SSE或Streamable HTTP传输将Agent运行时(Claude Desktop、Cursor、自定义编排器)连接到一个或多个MCP服务器时。
- 构建多租户平台,让多个用户共享同一MCP服务器时。
- 通过MCP资源传输敏感数据(个人身份信息、凭证、内部文档)时。
- 在受监管环境(SOC 2、HIPAA、PCI-DSS)中运行,要求工具调用可审计时。
如果你的MCP服务器仅在本地通过stdio运行,仅供单个开发者使用且无网络暴露,可以放宽部分传输层控制,但输入验证和审计日志仍需保留。
2. MCP Threat Model
2. MCP 威胁模型
Before hardening, understand what you are defending against.
| Threat | Vector | Impact |
|---|---|---|
| Unauthorized tool access | Agent calls tools the user should not have access to | Privilege escalation, data breach |
| Prompt injection via resources | Malicious content in MCP resources influences agent behavior | Arbitrary tool execution |
| Data exfiltration via results | Tool results leak sensitive data back to an untrusted agent | Data loss, compliance violation |
| SSRF via MCP tools | Agent tricks a tool into making internal network requests | Internal service compromise |
| Credential theft | API keys or tokens stored insecurely on the MCP server | Full account takeover |
| Denial of service | Agent floods server with tool calls or huge payloads | Service unavailability |
| Man-in-the-middle | Unencrypted transport between agent and MCP server | Eavesdropping, request tampering |
| Supply chain compromise | Malicious MCP server package or plugin | Arbitrary code execution |
在进行加固之前,先了解需要防御的对象。
| 威胁类型 | 攻击向量 | 影响 |
|---|---|---|
| 未授权工具访问 | Agent调用用户无权访问的工具 | 权限提升、数据泄露 |
| 通过资源进行提示注入 | MCP资源中的恶意内容影响Agent行为 | 任意工具执行 |
| 通过结果进行数据泄露 | 工具结果将敏感数据泄露给不可信Agent | 数据丢失、合规违规 |
| 通过MCP工具进行SSRF攻击 | Agent诱使工具发起内部网络请求 | 内部服务被入侵 |
| 凭证窃取 | MCP服务器上存储的API密钥或令牌不安全 | 账户完全被接管 |
| 拒绝服务攻击 | Agent向服务器发送大量工具调用或超大载荷 | 服务不可用 |
| 中间人攻击 | Agent与MCP服务器之间的传输未加密 | 窃听、请求篡改 |
| 供应链攻击 | 恶意MCP服务器包或插件 | 任意代码执行 |
3. Transport Security
3. 传输安全
3.1 TLS for Streamable HTTP and SSE Transports
3.1 针对Streamable HTTP和SSE传输的TLS配置
Every MCP server exposed over HTTP must terminate TLS. Never run plain HTTP in
production.
Nginx reverse proxy with TLS termination for an MCP server:
nginx
undefined所有通过HTTP暴露的MCP服务器必须终止TLS连接。生产环境中绝对不能使用明文HTTP。
为MCP服务器配置带TLS终止的Nginx反向代理:
nginx
undefined/etc/nginx/sites-enabled/mcp-server.conf
/etc/nginx/sites-enabled/mcp-server.conf
server {
listen 443 ssl http2;
server_name mcp.internal.example.com;
ssl_certificate /etc/ssl/certs/mcp-server.crt;
ssl_certificate_key /etc/ssl/private/mcp-server.key;
ssl_protocols TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
ssl_prefer_server_ciphers on;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS header
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE-specific: disable buffering so events stream immediately
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}}
undefinedserver {
listen 443 ssl http2;
server_name mcp.internal.example.com;
ssl_certificate /etc/ssl/certs/mcp-server.crt;
ssl_certificate_key /etc/ssl/private/mcp-server.key;
ssl_protocols TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
ssl_prefer_server_ciphers on;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS header
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE-specific: disable buffering so events stream immediately
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}}
undefined3.2 mTLS Between Agent and Server
3.2 Agent与服务器之间的mTLS配置
For high-security environments, require the agent (client) to present a certificate.
nginx
undefined对于高安全环境,要求Agent(客户端)提供证书。
nginx
undefinedAdd to the server block above
添加到上述server块中
ssl_client_certificate /etc/ssl/certs/agent-ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
**Generate a client certificate for an agent:**
```bashssl_client_certificate /etc/ssl/certs/agent-ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
**为Agent生成客户端证书:**
```bashCreate CA (one-time)
创建CA(一次性操作)
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256
-days 365 -noenc -keyout ca-key.pem -out ca-cert.pem
-subj "/CN=MCP Agent CA"
-days 365 -noenc -keyout ca-key.pem -out ca-cert.pem
-subj "/CN=MCP Agent CA"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256
-days 365 -noenc -keyout ca-key.pem -out ca-cert.pem
-subj "/CN=MCP Agent CA"
-days 365 -noenc -keyout ca-key.pem -out ca-cert.pem
-subj "/CN=MCP Agent CA"
Create agent client cert signed by the CA
创建由CA签名的Agent客户端证书
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256
-noenc -keyout agent-key.pem -out agent-csr.pem
-subj "/CN=agent-orchestrator-01"
-noenc -keyout agent-key.pem -out agent-csr.pem
-subj "/CN=agent-orchestrator-01"
openssl x509 -req -in agent-csr.pem -CA ca-cert.pem -CAkey ca-key.pem
-CAcreateserial -out agent-cert.pem -days 90
-CAcreateserial -out agent-cert.pem -days 90
undefinedopenssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256
-noenc -keyout agent-key.pem -out agent-csr.pem
-subj "/CN=agent-orchestrator-01"
-noenc -keyout agent-key.pem -out agent-csr.pem
-subj "/CN=agent-orchestrator-01"
openssl x509 -req -in agent-csr.pem -CA ca-cert.pem -CAkey ca-key.pem
-CAcreateserial -out agent-cert.pem -days 90
-CAcreateserial -out agent-cert.pem -days 90
undefined3.3 Securing stdio Transport
3.3 加固stdio传输
For local stdio-based servers, the attack surface is the process boundary itself:
jsonc
// claude_desktop_config.json - restrict stdio server permissions
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/safe-dir"],
"env": {
"NODE_OPTIONS": "--experimental-permission --allow-fs-read=/home/user/safe-dir --allow-fs-write=/home/user/safe-dir"
}
}
}
}对于基于本地stdio的服务器,攻击面是进程边界本身:
jsonc
// claude_desktop_config.json - 限制stdio服务器权限
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/safe-dir"],
"env": {
"NODE_OPTIONS": "--experimental-permission --allow-fs-read=/home/user/safe-dir --allow-fs-write=/home/user/safe-dir"
}
}
}
}4. Authentication and Authorization
4. 认证与授权
4.1 OAuth 2.1 Integration
4.1 OAuth 2.1 集成
MCP's Streamable HTTP transport supports OAuth 2.1 for client authentication. Configure
your server to validate bearer tokens on every request.
typescript
// src/auth.ts - OAuth 2.1 token validation middleware for an MCP server
import { createServer } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import jwt from "jsonwebtoken";
const app = express();
// OAuth 2.1 token validation middleware
function validateBearerToken(req: express.Request, res: express.Response, next: express.NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "missing_token" });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, process.env.OAUTH_PUBLIC_KEY!, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "mcp-server",
});
// Attach scopes for downstream authorization
(req as any).tokenScopes = (payload as any).scope?.split(" ") || [];
(req as any).userId = (payload as any).sub;
next();
} catch {
return res.status(403).json({ error: "invalid_token" });
}
}
app.use("/mcp", validateBearerToken);MCP的Streamable HTTP传输支持OAuth 2.1进行客户端认证。配置服务器以验证每个请求中的Bearer令牌。
typescript
// src/auth.ts - MCP服务器的OAuth 2.1令牌验证中间件
import { createServer } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import jwt from "jsonwebtoken";
const app = express();
// OAuth 2.1令牌验证中间件
function validateBearerToken(req: express.Request, res: express.Response, next: express.NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "missing_token" });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, process.env.OAUTH_PUBLIC_KEY!, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "mcp-server",
});
// 为下游授权附加权限范围
(req as any).tokenScopes = (payload as any).scope?.split(" ") || [];
(req as any).userId = (payload as any).sub;
next();
} catch {
return res.status(403).json({ error: "invalid_token" });
}
}
app.use("/mcp", validateBearerToken);4.2 API Key Management
4.2 API密钥管理
For simpler deployments, use hashed API keys stored server-side:
typescript
// src/apikeys.ts
import { createHash, timingSafeEqual } from "crypto";
interface ApiKeyRecord {
hashedKey: string;
userId: string;
allowedTools: string[];
rateLimit: number; // requests per minute
expiresAt: Date;
}
// Store hashed keys, never plaintext
const apiKeyStore: Map<string, ApiKeyRecord> = new Map();
export function registerApiKey(plainKey: string, record: Omit<ApiKeyRecord, "hashedKey">) {
const hashed = createHash("sha256").update(plainKey).digest("hex");
apiKeyStore.set(hashed, { ...record, hashedKey: hashed });
}
export function validateApiKey(plainKey: string): ApiKeyRecord | null {
const hashed = createHash("sha256").update(plainKey).digest("hex");
const record = apiKeyStore.get(hashed);
if (!record) return null;
if (new Date() > record.expiresAt) {
apiKeyStore.delete(hashed);
return null;
}
return record;
}对于更简单的部署,使用存储在服务器端的哈希API密钥:
typescript
// src/apikeys.ts
import { createHash, timingSafeEqual } from "crypto";
interface ApiKeyRecord {
hashedKey: string;
userId: string;
allowedTools: string[];
rateLimit: number; // 每分钟请求数
expiresAt: Date;
}
// 存储哈希密钥,绝不存储明文
const apiKeyStore: Map<string, ApiKeyRecord> = new Map();
export function registerApiKey(plainKey: string, record: Omit<ApiKeyRecord, "hashedKey">) {
const hashed = createHash("sha256").update(plainKey).digest("hex");
apiKeyStore.set(hashed, { ...record, hashedKey: hashed });
}
export function validateApiKey(plainKey: string): ApiKeyRecord | null {
const hashed = createHash("sha256").update(plainKey).digest("hex");
const record = apiKeyStore.get(hashed);
if (!record) return null;
if (new Date() > record.expiresAt) {
apiKeyStore.delete(hashed);
return null;
}
return record;
}5. Tool Authorization
5. 工具授权
5.1 Allowlist Patterns
5.1 允许列表模式
Never expose every tool to every user. Define an explicit allowlist:
yaml
undefined绝不为每个用户开放所有工具。定义明确的允许列表:
yaml
undefinedconfig/tool-policy.yaml
config/tool-policy.yaml
policies:
-
role: developer allowed_tools:
- "read_file"
- "search_code"
- "run_tests" denied_tools:
- "execute_command"
- "write_file"
- "delete_file"
-
role: admin allowed_tools: ["*"] denied_tools: []
-
role: readonly-agent allowed_tools:
- "read_file"
- "list_directory"
- "query_database:SELECT" denied_tools: ["*"]
dangerous_tools:
- name: "execute_command" risk: critical requires_approval: true max_executions_per_session: 5
- name: "write_file" risk: high requires_approval: true
- name: "query_database" risk: medium allowed_operations: ["SELECT"]
undefinedpolicies:
-
role: developer allowed_tools:
- "read_file"
- "search_code"
- "run_tests" denied_tools:
- "execute_command"
- "write_file"
- "delete_file"
-
role: admin allowed_tools: ["*"] denied_tools: []
-
role: readonly-agent allowed_tools:
- "read_file"
- "list_directory"
- "query_database:SELECT" denied_tools: ["*"]
dangerous_tools:
- name: "execute_command" risk: critical requires_approval: true max_executions_per_session: 5
- name: "write_file" risk: high requires_approval: true
- name: "query_database" risk: medium allowed_operations: ["SELECT"]
undefined5.2 Per-User Tool Access Enforcement
5.2 按用户执行工具访问控制
typescript
// src/toolAuth.ts
import { readFileSync } from "fs";
import { parse } from "yaml";
interface ToolPolicy {
role: string;
allowed_tools: string[];
denied_tools: string[];
}
const config = parse(readFileSync("config/tool-policy.yaml", "utf-8"));
const policies: ToolPolicy[] = config.policies;
export function isToolAllowed(userRole: string, toolName: string): boolean {
const policy = policies.find((p) => p.role === userRole);
if (!policy) return false;
// Explicit deny takes precedence
if (policy.denied_tools.includes(toolName)) return false;
if (policy.denied_tools.includes("*") && !policy.allowed_tools.includes(toolName)) return false;
// Check allow
if (policy.allowed_tools.includes("*")) return true;
if (policy.allowed_tools.includes(toolName)) return true;
return false;
}
// MCP server integration: wrap the tool handler
export function authorizedToolHandler(server: any) {
const originalCallTool = server.callTool.bind(server);
server.callTool = async (request: any, context: any) => {
const userRole = context.session?.userRole || "readonly-agent";
const toolName = request.params.name;
if (!isToolAllowed(userRole, toolName)) {
return {
content: [{ type: "text", text: `Access denied: tool "${toolName}" is not permitted for role "${userRole}".` }],
isError: true,
};
}
return originalCallTool(request, context);
};
}typescript
// src/toolAuth.ts
import { readFileSync } from "fs";
import { parse } from "yaml";
interface ToolPolicy {
role: string;
allowed_tools: string[];
denied_tools: string[];
}
const config = parse(readFileSync("config/tool-policy.yaml", "utf-8"));
const policies: ToolPolicy[] = config.policies;
export function isToolAllowed(userRole: string, toolName: string): boolean {
const policy = policies.find((p) => p.role === userRole);
if (!policy) return false;
// 明确拒绝优先
if (policy.denied_tools.includes(toolName)) return false;
if (policy.denied_tools.includes("*") && !policy.allowed_tools.includes(toolName)) return false;
// 检查允许列表
if (policy.allowed_tools.includes("*")) return true;
if (policy.allowed_tools.includes(toolName)) return true;
return false;
}
// MCP服务器集成:包装工具处理器
export function authorizedToolHandler(server: any) {
const originalCallTool = server.callTool.bind(server);
server.callTool = async (request: any, context: any) => {
const userRole = context.session?.userRole || "readonly-agent";
const toolName = request.params.name;
if (!isToolAllowed(userRole, toolName)) {
return {
content: [{ type: "text", text: `访问被拒绝:角色"${userRole}"无权使用工具"${toolName}"。` }],
isError: true,
};
}
return originalCallTool(request, context);
};
}6. Input Validation
6. 输入验证
6.1 JSON Schema for Tool Parameters
6.1 工具参数的JSON Schema
Define strict schemas for every tool's input. Reject anything that does not conform.
typescript
// src/validation.ts
import Ajv from "ajv";
import addFormats from "ajv-formats";
const ajv = new Ajv({ allErrors: true, removeAdditional: true });
addFormats(ajv);
// Schema registry keyed by tool name
const toolSchemas: Record<string, object> = {
read_file: {
type: "object",
properties: {
path: {
type: "string",
pattern: "^[a-zA-Z0-9_/\\-.]+$", // No path traversal chars
maxLength: 256,
},
},
required: ["path"],
additionalProperties: false,
},
query_database: {
type: "object",
properties: {
query: { type: "string", maxLength: 2048 },
database: { type: "string", enum: ["analytics", "public_catalog"] },
parameters: {
type: "array",
items: { type: ["string", "number", "boolean"] },
maxItems: 20,
},
},
required: ["query", "database"],
additionalProperties: false,
},
};
export function validateToolInput(toolName: string, params: unknown): { valid: boolean; errors?: string } {
const schema = toolSchemas[toolName];
if (!schema) return { valid: false, errors: `No schema registered for tool: ${toolName}` };
const validate = ajv.compile(schema);
if (validate(params)) return { valid: true };
const errorMsg = validate.errors?.map((e) => `${e.instancePath} ${e.message}`).join("; ");
return { valid: false, errors: errorMsg };
}为每个工具的输入定义严格的Schema。拒绝任何不符合Schema的输入。
typescript
// src/validation.ts
import Ajv from "ajv";
import addFormats from "ajv-formats";
const ajv = new Ajv({ allErrors: true, removeAdditional: true });
addFormats(ajv);
// 按工具名称索引的Schema注册表
const toolSchemas: Record<string, object> = {
read_file: {
type: "object",
properties: {
path: {
type: "string",
pattern: "^[a-zA-Z0-9_/\\-.]+$", // 无路径遍历字符
maxLength: 256,
},
},
required: ["path"],
additionalProperties: false,
},
query_database: {
type: "object",
properties: {
query: { type: "string", maxLength: 2048 },
database: { type: "string", enum: ["analytics", "public_catalog"] },
parameters: {
type: "array",
items: { type: ["string", "number", "boolean"] },
maxItems: 20,
},
},
required: ["query", "database"],
additionalProperties: false,
},
};
export function validateToolInput(toolName: string, params: unknown): { valid: boolean; errors?: string } {
const schema = toolSchemas[toolName];
if (!schema) return { valid: false, errors: `未为工具${toolName}注册Schema` };
const validate = ajv.compile(schema);
if (validate(params)) return { valid: true };
const errorMsg = validate.errors?.map((e) => `${e.instancePath} ${e.message}`).join("; ");
return { valid: false, errors: errorMsg };
}6.2 SQL Injection Prevention
6.2 SQL注入防护
Never pass raw agent-supplied strings into queries. Use parameterized queries and
statement-level restrictions.
typescript
// src/safeSql.ts
const FORBIDDEN_PATTERNS = [
/;\s*(DROP|ALTER|TRUNCATE|DELETE|UPDATE|INSERT|CREATE|GRANT|REVOKE)/i,
/UNION\s+SELECT/i,
/INTO\s+OUTFILE/i,
/LOAD_FILE\s*\(/i,
/xp_cmdshell/i,
];
export function sanitizeSqlQuery(query: string): { safe: boolean; reason?: string } {
for (const pattern of FORBIDDEN_PATTERNS) {
if (pattern.test(query)) {
return { safe: false, reason: `Query matches forbidden pattern: ${pattern}` };
}
}
// Only allow SELECT statements
const trimmed = query.trim().toUpperCase();
if (!trimmed.startsWith("SELECT")) {
return { safe: false, reason: "Only SELECT queries are permitted" };
}
return { safe: true };
}
// Usage in a database tool handler
export async function handleDatabaseQuery(params: { query: string; parameters?: any[] }, db: any) {
const check = sanitizeSqlQuery(params.query);
if (!check.safe) {
throw new Error(`Query rejected: ${check.reason}`);
}
// Always use parameterized execution
return db.query(params.query, params.parameters || []);
}绝不要将Agent提供的原始字符串直接传入查询。使用参数化查询和语句级限制。
typescript
// src/safeSql.ts
const FORBIDDEN_PATTERNS = [
/;\s*(DROP|ALTER|TRUNCATE|DELETE|UPDATE|INSERT|CREATE|GRANT|REVOKE)/i,
/UNION\s+SELECT/i,
/INTO\s+OUTFILE/i,
/LOAD_FILE\s*\(/i,
/xp_cmdshell/i,
];
export function sanitizeSqlQuery(query: string): { safe: boolean; reason?: string } {
for (const pattern of FORBIDDEN_PATTERNS) {
if (pattern.test(query)) {
return { safe: false, reason: `查询匹配禁止模式:${pattern}` };
}
}
// 仅允许SELECT语句
const trimmed = query.trim().toUpperCase();
if (!trimmed.startsWith("SELECT")) {
return { safe: false, reason: "仅允许SELECT查询" };
}
return { safe: true };
}
// 在数据库工具处理器中的用法
export async function handleDatabaseQuery(params: { query: string; parameters?: any[] }, db: any) {
const check = sanitizeSqlQuery(params.query);
if (!check.safe) {
throw new Error(`查询被拒绝:${check.reason}`);
}
// 始终使用参数化执行
return db.query(params.query, params.parameters || []);
}6.3 Filesystem Path Injection Prevention
6.3 文件系统路径注入防护
typescript
// src/safePath.ts
import path from "path";
const SANDBOX_ROOT = "/home/mcpuser/workspace";
export function resolveSafePath(userPath: string): string {
// Resolve to absolute, then verify it is inside the sandbox
const resolved = path.resolve(SANDBOX_ROOT, userPath);
if (!resolved.startsWith(SANDBOX_ROOT + path.sep) && resolved !== SANDBOX_ROOT) {
throw new Error(`Path traversal blocked: "${userPath}" resolves outside sandbox.`);
}
// Block symlink escape
const real = require("fs").realpathSync.native(resolved);
if (!real.startsWith(SANDBOX_ROOT)) {
throw new Error(`Symlink escape blocked: "${userPath}" -> "${real}"`);
}
return resolved;
}typescript
// src/safePath.ts
import path from "path";
const SANDBOX_ROOT = "/home/mcpuser/workspace";
export function resolveSafePath(userPath: string): string {
// 解析为绝对路径,然后验证其是否在沙箱内
const resolved = path.resolve(SANDBOX_ROOT, userPath);
if (!resolved.startsWith(SANDBOX_ROOT + path.sep) && resolved !== SANDBOX_ROOT) {
throw new Error(`路径遍历被阻止:"${userPath}"解析到沙箱外部。`);
}
// 阻止符号链接逃逸
const real = require("fs").realpathSync.native(resolved);
if (!real.startsWith(SANDBOX_ROOT)) {
throw new Error(`符号链接逃逸被阻止:"${userPath}" -> "${real}"`);
}
return resolved;
}7. Resource Access Control
7. 资源访问控制
7.1 Filesystem Sandboxing
7.1 文件系统沙箱
Use OS-level controls in addition to application-level path validation.
bash
#!/usr/bin/env bash除了应用级路径验证外,使用操作系统级控制。
bash
#!/usr/bin/env bashrun-mcp-sandboxed.sh - Launch MCP server with Linux namespace sandboxing
run-mcp-sandboxed.sh - 使用Linux命名空间沙箱启动MCP服务器
exec unshare --map-root-user --mount --pid --fork -- bash -c '
Create a read-only bind mount for the workspace
mount --bind /home/mcpuser/workspace /home/mcpuser/workspace
mount -o remount,ro,bind /home/mcpuser/workspace
Make the writable output directory available
mount --bind /home/mcpuser/output /home/mcpuser/output
Block access to sensitive host paths
mount -t tmpfs tmpfs /etc/ssh
mount -t tmpfs tmpfs /root
mount -t tmpfs tmpfs /home/mcpuser/.ssh
Run the MCP server
exec node /opt/mcp-server/dist/index.js
'
undefinedexec unshare --map-root-user --mount --pid --fork -- bash -c '
为工作区创建只读绑定挂载
mount --bind /home/mcpuser/workspace /home/mcpuser/workspace
mount -o remount,ro,bind /home/mcpuser/workspace
提供可写的输出目录
mount --bind /home/mcpuser/output /home/mcpuser/output
阻止访问敏感主机路径
mount -t tmpfs tmpfs /etc/ssh
mount -t tmpfs tmpfs /root
mount -t tmpfs tmpfs /home/mcpuser/.ssh
运行MCP服务器
exec node /opt/mcp-server/dist/index.js
'
undefined7.2 Database Query Restrictions
7.2 数据库查询限制
Create a dedicated read-only database user for MCP servers:
sql
-- PostgreSQL: MCP server database user
CREATE ROLE mcp_readonly WITH LOGIN PASSWORD 'use-a-vault-generated-secret';
-- Grant read-only access to specific schemas only
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;
-- Restrict row-level access where needed
ALTER TABLE customer_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY mcp_access ON customer_data
FOR SELECT TO mcp_readonly
USING (sensitivity_level < 3);
-- Set resource limits to prevent expensive queries
ALTER ROLE mcp_readonly SET statement_timeout = '10s';
ALTER ROLE mcp_readonly SET work_mem = '64MB';为MCP服务器创建专用的只读数据库用户:
sql
-- PostgreSQL: MCP服务器数据库用户
CREATE ROLE mcp_readonly WITH LOGIN PASSWORD 'use-a-vault-generated-secret';
-- 仅授予对特定模式的只读访问权限
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;
-- 根据需要限制行级访问
ALTER TABLE customer_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY mcp_access ON customer_data
FOR SELECT TO mcp_readonly
USING (sensitivity_level < 3);
-- 设置资源限制以防止昂贵的查询
ALTER ROLE mcp_readonly SET statement_timeout = '10s';
ALTER ROLE mcp_readonly SET work_mem = '64MB';7.3 Network Access Controls
7.3 网络访问控制
Prevent MCP tools from reaching internal services (SSRF mitigation):
typescript
// src/networkPolicy.ts
import { URL } from "url";
import net from "net";
const BLOCKED_CIDRS = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"127.0.0.0/8",
"169.254.169.254/32", // Cloud metadata endpoint
"fd00::/8",
];
const ALLOWED_DOMAINS = [
"api.github.com",
"registry.npmjs.org",
];
export function validateOutboundUrl(urlString: string): { allowed: boolean; reason?: string } {
let parsed: URL;
try {
parsed = new URL(urlString);
} catch {
return { allowed: false, reason: "Invalid URL" };
}
// Block non-HTTPS
if (parsed.protocol !== "https:") {
return { allowed: false, reason: "Only HTTPS is allowed" };
}
// Domain allowlist
if (!ALLOWED_DOMAINS.includes(parsed.hostname)) {
return { allowed: false, reason: `Domain ${parsed.hostname} is not in the allowlist` };
}
return { allowed: true };
}防止MCP工具访问内部服务(SSRF缓解):
typescript
// src/networkPolicy.ts
import { URL } from "url";
import net from "net";
const BLOCKED_CIDRS = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"127.0.0.0/8",
"169.254.169.254/32", // 云元数据端点
"fd00::/8",
];
const ALLOWED_DOMAINS = [
"api.github.com",
"registry.npmjs.org",
];
export function validateOutboundUrl(urlString: string): { allowed: boolean; reason?: string } {
let parsed: URL;
try {
parsed = new URL(urlString);
} catch {
return { allowed: false, reason: "无效URL" };
}
// 阻止非HTTPS协议
if (parsed.protocol !== "https:") {
return { allowed: false, reason: "仅允许HTTPS" };
}
// 域名允许列表
if (!ALLOWED_DOMAINS.includes(parsed.hostname)) {
return { allowed: false, reason: `域名${parsed.hostname}不在允许列表中` };
}
return { allowed: true };
}8. Rate Limiting
8. 速率限制
8.1 Per-Client Rate Limits
8.1 按客户端速率限制
typescript
// src/rateLimit.ts
interface RateBucket {
tokens: number;
lastRefill: number;
}
const buckets = new Map<string, RateBucket>();
const DEFAULT_RATE = 60; // requests per minute
const DEFAULT_BURST = 10; // max burst
export function checkRateLimit(
clientId: string,
maxPerMinute: number = DEFAULT_RATE,
burst: number = DEFAULT_BURST
): { allowed: boolean; retryAfterMs?: number } {
const now = Date.now();
let bucket = buckets.get(clientId);
if (!bucket) {
bucket = { tokens: burst, lastRefill: now };
buckets.set(clientId, bucket);
}
// Refill tokens based on elapsed time
const elapsed = now - bucket.lastRefill;
const refill = (elapsed / 60000) * maxPerMinute;
bucket.tokens = Math.min(burst, bucket.tokens + refill);
bucket.lastRefill = now;
if (bucket.tokens < 1) {
const waitMs = ((1 - bucket.tokens) / maxPerMinute) * 60000;
return { allowed: false, retryAfterMs: Math.ceil(waitMs) };
}
bucket.tokens -= 1;
return { allowed: true };
}typescript
// src/rateLimit.ts
interface RateBucket {
tokens: number;
lastRefill: number;
}
const buckets = new Map<string, RateBucket>();
const DEFAULT_RATE = 60; // 每分钟请求数
const DEFAULT_BURST = 10; // 最大突发请求数
export function checkRateLimit(
clientId: string,
maxPerMinute: number = DEFAULT_RATE,
burst: number = DEFAULT_BURST
): { allowed: boolean; retryAfterMs?: number } {
const now = Date.now();
let bucket = buckets.get(clientId);
if (!bucket) {
bucket = { tokens: burst, lastRefill: now };
buckets.set(clientId, bucket);
}
// 根据经过的时间补充令牌
const elapsed = now - bucket.lastRefill;
const refill = (elapsed / 60000) * maxPerMinute;
bucket.tokens = Math.min(burst, bucket.tokens + refill);
bucket.lastRefill = now;
if (bucket.tokens < 1) {
const waitMs = ((1 - bucket.tokens) / maxPerMinute) * 60000;
return { allowed: false, retryAfterMs: Math.ceil(waitMs) };
}
bucket.tokens -= 1;
return { allowed: true };
}8.2 Token Budget Enforcement
8.2 令牌预算执行
Limit how many LLM tokens a single session can consume through tool calls:
typescript
// src/tokenBudget.ts
interface SessionBudget {
usedInputTokens: number;
usedOutputTokens: number;
maxInputTokens: number;
maxOutputTokens: number;
}
const sessionBudgets = new Map<string, SessionBudget>();
export function initSessionBudget(sessionId: string, maxInput = 500_000, maxOutput = 100_000) {
sessionBudgets.set(sessionId, {
usedInputTokens: 0,
usedOutputTokens: 0,
maxInputTokens: maxInput,
maxOutputTokens: maxOutput,
});
}
export function consumeTokens(
sessionId: string,
inputTokens: number,
outputTokens: number
): { allowed: boolean; remaining: { input: number; output: number } } {
const budget = sessionBudgets.get(sessionId);
if (!budget) return { allowed: false, remaining: { input: 0, output: 0 } };
budget.usedInputTokens += inputTokens;
budget.usedOutputTokens += outputTokens;
const remaining = {
input: budget.maxInputTokens - budget.usedInputTokens,
output: budget.maxOutputTokens - budget.usedOutputTokens,
};
if (remaining.input < 0 || remaining.output < 0) {
return { allowed: false, remaining };
}
return { allowed: true, remaining };
}限制单个会话通过工具调用可消耗的LLM令牌数量:
typescript
// src/tokenBudget.ts
interface SessionBudget {
usedInputTokens: number;
usedOutputTokens: number;
maxInputTokens: number;
maxOutputTokens: number;
}
const sessionBudgets = new Map<string, SessionBudget>();
export function initSessionBudget(sessionId: string, maxInput = 500_000, maxOutput = 100_000) {
sessionBudgets.set(sessionId, {
usedInputTokens: 0,
usedOutputTokens: 0,
maxInputTokens: maxInput,
maxOutputTokens: maxOutput,
});
}
export function consumeTokens(
sessionId: string,
inputTokens: number,
outputTokens: number
): { allowed: boolean; remaining: { input: number; output: number } } {
const budget = sessionBudgets.get(sessionId);
if (!budget) return { allowed: false, remaining: { input: 0, output: 0 } };
budget.usedInputTokens += inputTokens;
budget.usedOutputTokens += outputTokens;
const remaining = {
input: budget.maxInputTokens - budget.usedInputTokens,
output: budget.maxOutputTokens - budget.usedOutputTokens,
};
if (remaining.input < 0 || remaining.output < 0) {
return { allowed: false, remaining };
}
return { allowed: true, remaining };
}9. Audit Logging
9. 审计日志
9.1 Structured Tool Invocation Logging
9.1 结构化工具调用日志
Log every tool call with full context. Never log raw secrets or credentials.
typescript
// src/auditLog.ts
import { randomUUID } from "crypto";
interface AuditEntry {
id: string;
timestamp: string;
userId: string;
sessionId: string;
toolName: string;
parameters: Record<string, unknown>;
result: "success" | "error" | "denied";
durationMs: number;
error?: string;
}
const REDACT_KEYS = ["password", "token", "secret", "api_key", "authorization"];
function redactSensitive(params: Record<string, unknown>): Record<string, unknown> {
const redacted: Record<string, unknown> = {};
for (const [key, value] of Object.entries(params)) {
if (REDACT_KEYS.some((k) => key.toLowerCase().includes(k))) {
redacted[key] = "[REDACTED]";
} else if (typeof value === "object" && value !== null) {
redacted[key] = redactSensitive(value as Record<string, unknown>);
} else {
redacted[key] = value;
}
}
return redacted;
}
export function logToolInvocation(entry: Omit<AuditEntry, "id" | "timestamp">): AuditEntry {
const full: AuditEntry = {
...entry,
id: randomUUID(),
timestamp: new Date().toISOString(),
parameters: redactSensitive(entry.parameters),
};
// Write structured JSON to stdout for collection by log aggregator
process.stdout.write(JSON.stringify(full) + "\n");
return full;
}记录每个工具调用的完整上下文。绝不记录原始密钥或凭证。
typescript
// src/auditLog.ts
import { randomUUID } from "crypto";
interface AuditEntry {
id: string;
timestamp: string;
userId: string;
sessionId: string;
toolName: string;
parameters: Record<string, unknown>;
result: "success" | "error" | "denied";
durationMs: number;
error?: string;
}
const REDACT_KEYS = ["password", "token", "secret", "api_key", "authorization"];
function redactSensitive(params: Record<string, unknown>): Record<string, unknown> {
const redacted: Record<string, unknown> = {};
for (const [key, value] of Object.entries(params)) {
if (REDACT_KEYS.some((k) => key.toLowerCase().includes(k))) {
redacted[key] = "[REDACTED]";
} else if (typeof value === "object" && value !== null) {
redacted[key] = redactSensitive(value as Record<string, unknown>);
} else {
redacted[key] = value;
}
}
return redacted;
}
export function logToolInvocation(entry: Omit<AuditEntry, "id" | "timestamp">): AuditEntry {
const full: AuditEntry = {
...entry,
id: randomUUID(),
timestamp: new Date().toISOString(),
parameters: redactSensitive(entry.parameters),
};
// 将结构化JSON写入stdout,供日志聚合器收集
process.stdout.write(JSON.stringify(full) + "\n");
return full;
}9.2 OpenTelemetry Integration
9.2 OpenTelemetry集成
typescript
// src/otelTracing.ts
import { trace, SpanStatusCode, context, propagation } from "@opentelemetry/api";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { Resource } from "@opentelemetry/resources";
const provider = new NodeTracerProvider({
resource: new Resource({
"service.name": "mcp-server",
"service.version": "1.0.0",
}),
});
provider.addSpanProcessor(
new BatchSpanProcessor(
new OTLPTraceExporter({ url: "http://otel-collector:4318/v1/traces" })
)
);
provider.register();
const tracer = trace.getTracer("mcp-server");
export async function traceToolCall<T>(
toolName: string,
userId: string,
params: Record<string, unknown>,
fn: () => Promise<T>
): Promise<T> {
return tracer.startActiveSpan(`mcp.tool.${toolName}`, async (span) => {
span.setAttribute("mcp.tool.name", toolName);
span.setAttribute("mcp.user.id", userId);
span.setAttribute("mcp.params.keys", Object.keys(params).join(","));
try {
const result = await fn();
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (err: any) {
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
span.recordException(err);
throw err;
} finally {
span.end();
}
});
}typescript
// src/otelTracing.ts
import { trace, SpanStatusCode, context, propagation } from "@opentelemetry/api";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { Resource } from "@opentelemetry/resources";
const provider = new NodeTracerProvider({
resource: new Resource({
"service.name": "mcp-server",
"service.version": "1.0.0",
}),
});
provider.addSpanProcessor(
new BatchSpanProcessor(
new OTLPTraceExporter({ url: "http://otel-collector:4318/v1/traces" })
)
);
provider.register();
const tracer = trace.getTracer("mcp-server");
export async function traceToolCall<T>(
toolName: string,
userId: string,
params: Record<string, unknown>,
fn: () => Promise<T>
): Promise<T> {
return tracer.startActiveSpan(`mcp.tool.${toolName}`, async (span) => {
span.setAttribute("mcp.tool.name", toolName);
span.setAttribute("mcp.user.id", userId);
span.setAttribute("mcp.params.keys", Object.keys(params).join(","));
try {
const result = await fn();
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (err: any) {
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
span.recordException(err);
throw err;
} finally {
span.end();
}
});
}10. Deployment Hardening
10. 部署加固
10.1 Docker Container Configuration
10.1 Docker容器配置
dockerfile
undefineddockerfile
undefinedDockerfile.mcp-server
Dockerfile.mcp-server
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY src/ src/
COPY tsconfig.json ./
RUN npm run build
FROM node:22-slim
RUN groupadd -r mcp && useradd -r -g mcp -d /home/mcp -s /usr/sbin/nologin mcp
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY src/ src/
COPY tsconfig.json ./
RUN npm run build
FROM node:22-slim
RUN groupadd -r mcp && useradd -r -g mcp -d /home/mcp -s /usr/sbin/nologin mcp
Remove unnecessary packages
删除不必要的包
RUN apt-get purge -y --auto-remove curl wget &&
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
RUN apt-get purge -y --auto-remove curl wget &&
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
Create minimal writable directories
创建最小化的可写目录
RUN mkdir -p /home/mcp/workspace /home/mcp/output &&
chown -R mcp:mcp /home/mcp
chown -R mcp:mcp /home/mcp
USER mcp
RUN mkdir -p /home/mcp/workspace /home/mcp/output &&
chown -R mcp:mcp /home/mcp
chown -R mcp:mcp /home/mcp
USER mcp
Health check
健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3
CMD node -e "fetch('http://localhost:3001/health').then(r => process.exit(r.ok ? 0 : 1))"
CMD node -e "fetch('http://localhost:3001/health').then(r => process.exit(r.ok ? 0 : 1))"
EXPOSE 3001
CMD ["node", "dist/index.js"]
undefinedHEALTHCHECK --interval=30s --timeout=5s --retries=3
CMD node -e "fetch('http://localhost:3001/health').then(r => process.exit(r.ok ? 0 : 1))"
CMD node -e "fetch('http://localhost:3001/health').then(r => process.exit(r.ok ? 0 : 1))"
EXPOSE 3001
CMD ["node", "dist/index.js"]
undefined10.2 Kubernetes Network Policy
10.2 Kubernetes网络策略
yaml
undefinedyaml
undefinedk8s/network-policy.yaml
k8s/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: mcp-server-policy
namespace: ai-platform
spec:
podSelector:
matchLabels:
app: mcp-server
policyTypes:
- Ingress
- Egress
ingress:
# Only allow traffic from the agent orchestrator
- from:
- podSelector:
matchLabels:
app: agent-orchestrator
ports:
- protocol: TCP
port: 3001
egress:
# Allow DNS
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
# Allow access to the internal database
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
# Allow HTTPS outbound to specific external APIs
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- protocol: TCP
port: 443
undefinedapiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: mcp-server-policy
namespace: ai-platform
spec:
podSelector:
matchLabels:
app: mcp-server
policyTypes:
- Ingress
- Egress
ingress:
# 仅允许来自Agent编排器的流量
- from:
- podSelector:
matchLabels:
app: agent-orchestrator
ports:
- protocol: TCP
port: 3001
egress:
# 允许DNS
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
# 允许访问内部数据库
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
# 允许HTTPS出站到特定外部API
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- protocol: TCP
port: 443
undefined10.3 Seccomp Profile
10.3 Seccomp配置文件
json
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": [
"read", "write", "close", "fstat", "lseek", "mmap", "mprotect",
"munmap", "brk", "rt_sigaction", "rt_sigprocmask", "ioctl",
"access", "pipe", "select", "sched_yield", "mremap", "madvise",
"dup", "dup2", "nanosleep", "getpid", "socket", "connect",
"accept", "sendto", "recvfrom", "bind", "listen", "getsockname",
"getpeername", "setsockopt", "getsockopt", "clone", "execve",
"exit", "wait4", "fcntl", "getdents64", "getcwd", "chdir",
"openat", "newfstatat", "readlinkat", "exit_group", "epoll_create1",
"epoll_ctl", "epoll_wait", "eventfd2", "futex", "set_robust_list",
"clock_gettime", "getrandom", "statx", "pread64", "pwrite64"
],
"action": "SCMP_ACT_ALLOW"
}
]
}Apply the seccomp profile in your Kubernetes pod spec:
yaml
undefinedjson
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": [
"read", "write", "close", "fstat", "lseek", "mmap", "mprotect",
"munmap", "brk", "rt_sigaction", "rt_sigprocmask", "ioctl",
"access", "pipe", "select", "sched_yield", "mremap", "madvise",
"dup", "dup2", "nanosleep", "getpid", "socket", "connect",
"accept", "sendto", "recvfrom", "bind", "listen", "getsockname",
"getpeername", "setsockopt", "getsockopt", "clone", "execve",
"exit", "wait4", "fcntl", "getdents64", "getcwd", "chdir",
"openat", "newfstatat", "readlinkat", "exit_group", "epoll_create1",
"epoll_ctl", "epoll_wait", "eventfd2", "futex", "set_robust_list",
"clock_gettime", "getrandom", "statx", "pread64", "pwrite64"
],
"action": "SCMP_ACT_ALLOW"
}
]
}在Kubernetes Pod规格中应用Seccomp配置文件:
yaml
undefinedk8s/deployment.yaml (relevant snippet)
k8s/deployment.yaml (相关片段)
spec:
containers:
- name: mcp-server
image: registry.example.com/mcp-server:1.0
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: Localhost
localhostProfile: profiles/mcp-server-seccomp.json
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "250m"
---spec:
containers:
- name: mcp-server
image: registry.example.com/mcp-server:1.0
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: Localhost
localhostProfile: profiles/mcp-server-seccomp.json
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "250m"
---11. Testing
11. 测试
11.1 Authorization Boundary Tests
11.1 授权边界测试
typescript
// tests/toolAuth.test.ts
import { describe, it, expect } from "vitest";
import { isToolAllowed } from "../src/toolAuth";
describe("Tool Authorization", () => {
it("should deny developer access to execute_command", () => {
expect(isToolAllowed("developer", "execute_command")).toBe(false);
});
it("should allow developer access to read_file", () => {
expect(isToolAllowed("developer", "read_file")).toBe(true);
});
it("should allow admin access to everything", () => {
expect(isToolAllowed("admin", "execute_command")).toBe(true);
expect(isToolAllowed("admin", "delete_file")).toBe(true);
});
it("should deny unknown roles", () => {
expect(isToolAllowed("unknown-role", "read_file")).toBe(false);
});
it("should deny readonly-agent access to write_file", () => {
expect(isToolAllowed("readonly-agent", "write_file")).toBe(false);
});
});typescript
// tests/toolAuth.test.ts
import { describe, it, expect } from "vitest";
import { isToolAllowed } from "../src/toolAuth";
describe("工具授权", () => {
it("应拒绝开发者访问execute_command", () => {
expect(isToolAllowed("developer", "execute_command")).toBe(false);
});
it("应允许开发者访问read_file", () => {
expect(isToolAllowed("developer", "read_file")).toBe(true);
});
it("应允许管理员访问所有工具", () => {
expect(isToolAllowed("admin", "execute_command")).toBe(true);
expect(isToolAllowed("admin", "delete_file")).toBe(true);
});
it("应拒绝未知角色的访问", () => {
expect(isToolAllowed("unknown-role", "read_file")).toBe(false);
});
it("应拒绝只读Agent访问write_file", () => {
expect(isToolAllowed("readonly-agent", "write_file")).toBe(false);
});
});11.2 Input Validation Fuzz Tests
11.2 输入验证模糊测试
typescript
// tests/validation.test.ts
import { describe, it, expect } from "vitest";
import { validateToolInput } from "../src/validation";
describe("Input Validation", () => {
const maliciousInputs = [
{ path: "../../../etc/passwd" },
{ path: "/etc/shadow" },
{ path: "file\x00.txt" },
{ path: "a".repeat(1000) },
{ path: "valid.txt", extraField: "injected" },
];
for (const input of maliciousInputs) {
it(`should reject malicious read_file input: ${JSON.stringify(input)}`, () => {
const result = validateToolInput("read_file", input);
expect(result.valid).toBe(false);
});
}
it("should accept valid read_file input", () => {
const result = validateToolInput("read_file", { path: "src/index.ts" });
expect(result.valid).toBe(true);
});
const sqlInjections = [
{ query: "SELECT 1; DROP TABLE users;", database: "analytics" },
{ query: "SELECT * FROM t UNION SELECT password FROM users", database: "analytics" },
{ query: "DELETE FROM users WHERE 1=1", database: "analytics" },
];
for (const input of sqlInjections) {
it(`should reject SQL injection: ${input.query.slice(0, 40)}...`, () => {
const result = validateToolInput("query_database", input);
// Even if schema validation passes, the SQL sanitizer should catch it
expect(result.valid).toBe(true); // schema is valid
// The SQL check happens at the handler level - tested separately
});
}
});typescript
// tests/validation.test.ts
import { describe, it, expect } from "vitest";
import { validateToolInput } from "../src/validation";
describe("输入验证", () => {
const maliciousInputs = [
{ path: "../../../etc/passwd" },
{ path: "/etc/shadow" },
{ path: "file\x00.txt" },
{ path: "a".repeat(1000) },
{ path: "valid.txt", extraField: "injected" },
];
for (const input of maliciousInputs) {
it(`应拒绝恶意read_file输入:${JSON.stringify(input)}`, () => {
const result = validateToolInput("read_file", input);
expect(result.valid).toBe(false);
});
}
it("应接受有效的read_file输入", () => {
const result = validateToolInput("read_file", { path: "src/index.ts" });
expect(result.valid).toBe(true);
});
const sqlInjections = [
{ query: "SELECT 1; DROP TABLE users;", database: "analytics" },
{ query: "SELECT * FROM t UNION SELECT password FROM users", database: "analytics" },
{ query: "DELETE FROM users WHERE 1=1", database: "analytics" },
];
for (const input of sqlInjections) {
it(`应拒绝SQL注入:${input.query.slice(0, 40)}...`, () => {
const result = validateToolInput("query_database", input);
// 即使Schema验证通过,SQL清理器也应捕获它
expect(result.valid).toBe(true); // Schema有效
// SQL检查在处理器层面进行 - 单独测试
});
}
});11.3 SSRF Prevention Tests
11.3 SSRF防护测试
typescript
// tests/networkPolicy.test.ts
import { describe, it, expect } from "vitest";
import { validateOutboundUrl } from "../src/networkPolicy";
describe("Outbound URL Validation", () => {
const blockedUrls = [
"http://169.254.169.254/latest/meta-data/",
"http://localhost:8080/admin",
"https://evil.com/exfiltrate",
"ftp://internal-server/data",
"https://10.0.0.1:8443/internal",
"http://[::1]/admin",
];
for (const url of blockedUrls) {
it(`should block: ${url}`, () => {
expect(validateOutboundUrl(url).allowed).toBe(false);
});
}
it("should allow requests to explicitly allowed domains", () => {
expect(validateOutboundUrl("https://api.github.com/repos").allowed).toBe(true);
expect(validateOutboundUrl("https://registry.npmjs.org/express").allowed).toBe(true);
});
});typescript
// tests/networkPolicy.test.ts
import { describe, it, expect } from "vitest";
import { validateOutboundUrl } from "../src/networkPolicy";
describe("出站URL验证", () => {
const blockedUrls = [
"http://169.254.169.254/latest/meta-data/",
"http://localhost:8080/admin",
"https://evil.com/exfiltrate",
"ftp://internal-server/data",
"https://10.0.0.1:8443/internal",
"http://[::1]/admin",
];
for (const url of blockedUrls) {
it(`应阻止:${url}`, () => {
expect(validateOutboundUrl(url).allowed).toBe(false);
});
}
it("应允许请求到明确允许的域名", () => {
expect(validateOutboundUrl("https://api.github.com/repos").allowed).toBe(true);
expect(validateOutboundUrl("https://registry.npmjs.org/express").allowed).toBe(true);
});
});11.4 Rate Limit Tests
11.4 速率限制测试
typescript
// tests/rateLimit.test.ts
import { describe, it, expect } from "vitest";
import { checkRateLimit } from "../src/rateLimit";
describe("Rate Limiting", () => {
it("should allow requests within the burst limit", () => {
const clientId = "test-burst-" + Date.now();
for (let i = 0; i < 10; i++) {
expect(checkRateLimit(clientId).allowed).toBe(true);
}
});
it("should deny requests exceeding the burst limit", () => {
const clientId = "test-exceed-" + Date.now();
for (let i = 0; i < 10; i++) {
checkRateLimit(clientId);
}
const result = checkRateLimit(clientId);
expect(result.allowed).toBe(false);
expect(result.retryAfterMs).toBeGreaterThan(0);
});
});typescript
// tests/rateLimit.test.ts
import { describe, it, expect } from "vitest";
import { checkRateLimit } from "../src/rateLimit";
describe("速率限制", () => {
it("应允许在突发限制内的请求", () => {
const clientId = "test-burst-" + Date.now();
for (let i = 0; i < 10; i++) {
expect(checkRateLimit(clientId).allowed).toBe(true);
}
});
it("应拒绝超过突发限制的请求", () => {
const clientId = "test-exceed-" + Date.now();
for (let i = 0; i < 10; i++) {
checkRateLimit(clientId);
}
const result = checkRateLimit(clientId);
expect(result.allowed).toBe(false);
expect(result.retryAfterMs).toBeGreaterThan(0);
});
});Quick Reference Checklist
快速参考清单
Use this checklist when deploying any MCP server to production:
[ ] TLS 1.3 enabled on all HTTP transports
[ ] mTLS configured for server-to-server communication
[ ] OAuth 2.1 or API key authentication enforced on every endpoint
[ ] Tool allowlist defined per role/user
[ ] Dangerous tools flagged and require explicit approval
[ ] JSON Schema validation on every tool's parameters
[ ] SQL queries use parameterized statements and are restricted to SELECT
[ ] Filesystem access sandboxed to explicit directories
[ ] Outbound network requests limited to an allowlist (SSRF mitigation)
[ ] Per-client rate limits and session token budgets enforced
[ ] Every tool invocation logged with user, params (redacted), and result
[ ] OpenTelemetry tracing integrated for observability
[ ] Container runs as non-root with read-only filesystem
[ ] Seccomp profile applied to restrict syscalls
[ ] Network policies restrict pod-to-pod communication
[ ] Authorization and input validation tests pass in CI将MCP服务器部署到生产环境时使用本清单:
[ ] 所有HTTP传输已启用TLS 1.3
[ ] 服务器到服务器通信已配置mTLS
[ ] 每个端点都强制使用OAuth 2.1或API密钥认证
[ ] 按角色/用户定义了工具允许列表
[ ] 危险工具已标记并需要明确批准
[ ] 每个工具的参数都进行JSON Schema验证
[ ] SQL查询使用参数化语句并限制为SELECT
[ ] 文件系统访问被沙箱化到明确目录
[ ] 出站网络请求限制在允许列表内(SSRF缓解)
[ ] 执行按客户端速率限制和会话令牌预算
[ ] 每个工具调用都记录了用户、参数(已脱敏)和结果
[ ] 集成了OpenTelemetry追踪以实现可观测性
[ ] 容器以非root用户运行且文件系统为只读
[ ] 应用了Seccomp配置文件以限制系统调用
[ ] 网络策略限制了Pod到Pod的通信
[ ] 授权和输入验证测试在CI中通过