Back to Blog

MCP Server Security: Hardening Guide

Ultrion TeamJuly 18, 202613 min read

MCP Server Security: Hardening Guide

How to secure your MCP servers against attacks and abuse.


MCP servers are the gateway between AI agents and your tools/data. Securing them is critical β€” a compromised MCP server can expose sensitive data, enable unauthorized actions, and drain your budget. This guide covers everything you need to know.


Threat Model

Attack Vectors Against MCP Servers:
β”œβ”€β”€ Unauthorized access (no auth)
β”œβ”€β”€ Prompt injection via tool inputs
β”œβ”€β”€ Parameter manipulation
β”œβ”€β”€ Resource exhaustion (DoS/DoW)
β”œβ”€β”€ Data exfiltration via tool outputs
β”œβ”€β”€ Supply chain attacks
β”œβ”€β”€ Man-in-the-middle attacks
└── Privilege escalation

Security Checklist

Authentication

  • Require API key for every request
  • Use strong key generation (256-bit minimum)
  • Implement key rotation policy
  • Rate limit authentication attempts
  • Support OAuth 2.0 for enterprise

Authorization

  • Role-based access control (RBAC)
  • Per-tool permissions
  • Per-user rate limits
  • Budget limits per user
  • Admin-only tools

Input Security

  • Validate all inputs with JSON Schema
  • Sanitize string inputs
  • Enforce max input length
  • Detect prompt injection in inputs
  • Reject suspicious patterns

Output Security

  • Filter sensitive data from outputs
  • Enforce max output length
  • Log all outputs for audit
  • Scan for data leakage

Infrastructure

  • HTTPS only (no HTTP)
  • CORS properly configured
  • Firewall rules configured
  • Running as non-root user
  • Resource limits (CPU, memory)
  • Regular security updates

Implementing Authentication

API Key Authentication

import crypto from "crypto";

class AuthManager {
  private keys = new Map<string, { userId: string; permissions: string[] }>();

  generateKey(userId: string, permissions: string[]): string {
    const key = `sk-mcp-${crypto.randomBytes(32).toString("hex")}`;
    this.keys.set(key, { userId, permissions });
    return key;
  }

  authenticate(request: Request): { userId: string; permissions: string[] } | null {
    const apiKey = request.headers.get("x-api-key");
    if (!apiKey) return null;

    const keyData = this.keys.get(apiKey);
    if (!keyData) return null;

    return keyData;
  }
}

// Middleware
const auth = new AuthManager();

server.middleware(async (request, next) => {
  const user = auth.authenticate(request);
  if (!user) {
    return new Response("Unauthorized", { status: 401 });
  }
  request.user = user;
  return next();
});

OAuth 2.0 Integration

import jwt from "jsonwebtoken";

class OAuthMiddleware {
  async verify(request: Request): Promise<User | null> {
    const token = request.headers.get("authorization")?.replace("Bearer ", "");
    if (!token) return null;

    try {
      const payload = jwt.verify(token, process.env.JWT_SECRET!);
      return {
        userId: payload.sub,
        permissions: payload.permissions || [],
        orgId: payload.org_id,
      };
    } catch {
      return null;
    }
  }
}

Input Validation

Schema-Based Validation

import { z } from "zod";

// Define strict input schemas
const searchSchema = z.object({
  query: z.string()
    .min(1)
    .max(500)
    .refine(s => !containsInjection(s), "Invalid input"),
  maxResults: z.number().int().min(1).max(50).default(10),
  filters: z.object({
    dateFrom: z.string().datetime().optional(),
    dateTo: z.string().datetime().optional(),
  }).optional(),
});

server.tool("search", {
  query: z.string().max(500),
  maxResults: z.number().max(50).default(10),
}, async (args, context) => {
  // Validate
  const validated = searchSchema.parse(args);

  // Execute
  const results = await search(validated.query, validated.maxResults);

  // Validate output
  return {
    content: [{
      type: "text",
      text: JSON.stringify(results.slice(0, validated.maxResults)),
    }],
  };
});

Prompt Injection Detection

class InjectionDetector:
    SUSPICIOUS_PATTERNS = [
        r"ignore\s+(previous|all|your)\s+instructions",
        r"system\s+(prompt|instruction|override)",
        r"reveal\s+(your|system)\s+(prompt|instructions)",
        r"you\s+are\s+(now|actually)\s+(dan|evil|unrestricted)",
        r"disregard\s+(everything|all|previous)",
        r"\[system\]|\[admin\]|\[developer\]",
        r"new\s+(directive|instruction|rule)",
    ]

    def check(self, text: str) -> tuple[bool, str]:
        """Returns (is_safe, reason)."""
        normalized = text.lower()

        for pattern in self.SUSPICIOUS_PATTERNS:
            if re.search(pattern, normalized):
                return False, f"Matched suspicious pattern: {pattern}"

        if len(text) > 10000:
            return False, "Input exceeds maximum length"

        return True, "OK"

Rate Limiting

class RateLimiter {
  private limits = new Map<string, { count: number; resetAt: number }>();

  constructor(
    private maxRequests: number = 100,
    private windowMs: number = 60000, // 1 minute
  ) {}

  async check(identifier: string): Promise<boolean> {
    const key = identifier;
    const now = Date.now();

    let bucket = this.limits.get(key);

    if (!bucket || now > bucket.resetAt) {
      bucket = { count: 0, resetAt: now + this.windowMs };
      this.limits.set(key, bucket);
    }

    bucket.count++;

    if (bucket.count > this.maxRequests) {
      return false; // Rate limited
    }

    return true;
  }
}

// Per-user, per-tool rate limiting
class ToolRateLimit {
  private limits: Record<string, Record<string, RateLimit>> = {
    "send_email": { "per_user": { max: 10, window: "hour" } },
    "process_payment": { "per_user": { max: 3, window: "hour" } },
    "query_database": { "per_user": { max: 100, window: "hour" } },
    "search_web": { "per_user": { max: 50, window: "hour" } },
  };

  async check(userId: string, toolName: string): Promise<boolean> {
    const limit = this.limits[toolName]?.per_user;
    if (!limit) return true; // No limit configured

    const key = `${userId}:${toolName}`;
    return await this.redis.checkRate(key, limit.max, limit.window);
  }
}

Output Filtering

class OutputFilter:
    """Filter sensitive data from tool outputs."""

    SENSITIVE_PATTERNS = {
        "email": r'\b[\w.-]+@[\w.-]+\.\w+\b',
        "phone": r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b',
        "credit_card": r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b',
        "ssn": r'\b\d{3}-\d{2}-\d{4}\b',
        "api_key": r'(?:api[_-]?key|token|secret)["\s:=]+([A-Za-z0-9_-]{20,})',
        "ip_address": r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b',
    }

    def filter(self, output: str, level: str = "medium") -> str:
        """Filter sensitive data based on level."""
        import re

        patterns_to_check = self.SENSITIVE_PATTERNS
        if level == "strict":
            # Also redact names, addresses, etc.
            patterns_to_check = {**self.SENSITIVE_PATTERNS,
                "name": r'\b[A-Z][a-z]+ [A-Z][a-z]+\b'}

        for data_type, pattern in patterns_to_check.items():
            output = re.sub(pattern, f"[REDACTED_{data_type.upper()}]", output)

        return output

    def truncate(self, output: str, max_length: int = 5000) -> str:
        """Prevent output from being too large."""
        if len(output) > max_length:
            return output[:max_length] + "\n[... output truncated ...]"
        return output

Audit Logging

class AuditLogger:
    async def log_tool_call(self, event):
        """Log every tool call for security audit."""
        await self.db.insert("audit_log", {
            "timestamp": datetime.utcnow(),
            "user_id": event.user_id,
            "tool_name": event.tool_name,
            "input_hash": hash(str(event.input)),  # Don't store raw input
            "input_size": len(str(event.input)),
            "output_size": len(str(event.output)),
            "success": event.success,
            "error": event.error,
            "duration_ms": event.duration_ms,
            "cost_eur": event.cost,
            "ip_address": event.ip_address,
            "user_agent": event.user_agent,
        })

    async def detect_anomalies(self):
        """Check for suspicious patterns in audit log."""
        anomalies = []

        # Unusual request volume
        burst = await self.check_burst_activity()
        if burst:
            anomalies.append({"type": "burst", "details": burst})

        # Unusual tool combinations
        suspicious_chain = await self.check_tool_chains()
        if suspicious_chain:
            anomalies.append({"type": "suspicious_chain", "details": suspicious_chain})

        # Off-hours activity
        night_activity = await self.check_off_hours()
        if night_activity:
            anomalies.append({"type": "off_hours", "details": night_activity})

        return anomalies

Deployment Security

Docker Hardening

# Secure Docker image
FROM node:20-slim

# Create non-root user
RUN groupadd -r mcp && useradd -r -g mcp mcp
USER mcp

# Set working directory
WORKDIR /app

# Copy only necessary files
COPY --chown=mcp:mcp package*.json ./
RUN npm ci --production

COPY --chown=mcp:mcp dist/ ./dist/

# No shell, minimal packages
RUN rm -rf /var/lib/apt/lists/* /tmp/*

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD node -e "require('http').get('http://localhost:8000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"

EXPOSE 8000
CMD ["node", "dist/index.js"]

Environment Security

# .env.production β€” never commit this
MCP_API_KEY=sk-mcp-xxx
JWT_SECRET=xxx
DATABASE_URL=postgresql://...
ALLOWED_ORIGINS=https://skillexchange.market
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=60000
MAX_INPUT_LENGTH=10000
MAX_OUTPUT_LENGTH=5000
AUDIT_LOG_ENABLED=true
SENSITIVE_DATA_FILTER=strict

Conclusion

Securing your MCP server is non-negotiable for production deployments. By implementing authentication, input validation, rate limiting, output filtering, and audit logging, you create multiple layers of defense against attacks.

Security is an ongoing process β€” regularly review logs, test with penetration tests, and update your defenses as new threats emerge.


Learn More

Find security-focused MCP tools on SkillExchange.

Newsletter

Enjoying this article?

Get weekly insights on building and selling AI skills, MCP tools, and creator economics. Join 2,000+ AI builders and creators.

No spam. Unsubscribe anytime.

Get the Free MCP Server Handbook

50+ pages of practical guides, code examples, and production-ready templates.

  • Complete MCP protocol reference
  • 15+ production-ready templates
  • Security best practices guide

No spam. Unsubscribe anytime. We respect your privacy.

Related Articles

Ready to try AI skills?

Browse the marketplace and discover skills for your AI agents.

Browse Skills