Chapter 9: Security and Access Control
Haiyue
57min
Chapter 9: Security and Access Control
Learning Objectives
- Understand the MCP security model and threat protection
- Implement authentication and authorization mechanisms
- Master resource access control
- Learn input validation and data sanitization
- Implement security auditing and monitoring
9.1 MCP Security Model and Threat Analysis
9.1.1 Security Threat Model
The main security threats faced by the MCP Server:
// src/security/ThreatModel.ts
export enum ThreatCategory {
AUTHENTICATION = 'authentication',
AUTHORIZATION = 'authorization',
INPUT_VALIDATION = 'input_validation',
DATA_EXPOSURE = 'data_exposure',
RESOURCE_ACCESS = 'resource_access',
DENIAL_OF_SERVICE = 'denial_of_service',
CODE_INJECTION = 'code_injection',
PRIVILEGE_ESCALATION = 'privilege_escalation',
}
export interface SecurityThreat {
id: string;
category: ThreatCategory;
severity: 'low' | 'medium' | 'high' | 'critical';
description: string;
impact: string;
mitigations: string[];
examples?: string[];
}
export const COMMON_THREATS: SecurityThreat[] = [
{
id: 'UNAUTH_ACCESS',
category: ThreatCategory.AUTHENTICATION,
severity: 'critical',
description: 'Unauthorized access',
impact: 'Malicious users may gain unauthorized access to system resources',
mitigations: [
'Implement mandatory authentication',
'Use secure authentication protocols',
'Implement session management',
'Enable account lockout mechanisms'
],
examples: [
'Direct access to sensitive tools without verification',
'Bypassing authentication mechanisms to access resources'
]
},
{
id: 'PRIVILEGE_ESC',
category: ThreatCategory.PRIVILEGE_ESCALATION,
severity: 'high',
description: 'Privilege escalation attack',
impact: 'Low-privilege users gain high-privilege operation capabilities',
mitigations: [
'Implement the principle of least privilege',
'Regularly audit permission assignments',
'Use role-based access control',
'Implement permission boundary checks'
]
},
{
id: 'INJECTION_ATTACK',
category: ThreatCategory.CODE_INJECTION,
severity: 'critical',
description: 'Code injection attack',
impact: 'Malicious code execution can lead to complete system control',
mitigations: [
'Strict input validation',
'Use parameterized queries',
'Implement input sanitization',
'Code execution sandboxing'
],
examples: [
'SQL injection attack',
'Command injection attack',
'Script injection attack'
]
},
{
id: 'DATA_LEAK',
category: ThreatCategory.DATA_EXPOSURE,
severity: 'high',
description: 'Sensitive data leakage',
impact: 'Confidential information may be accessed or leaked without authorization',
mitigations: [
'Data classification and tagging',
'Access control policies',
'Data encryption at rest',
'Audit log monitoring'
]
},
{
id: 'DOS_ATTACK',
category: ThreatCategory.DENIAL_OF_SERVICE,
severity: 'medium',
description: 'Denial of service attack',
impact: 'Service availability is affected',
mitigations: [
'Request rate limiting',
'Resource usage monitoring',
'Request size limiting',
'Connection count limiting'
]
}
];
export class SecurityAssessment {
private threats: Map<string, SecurityThreat> = new Map();
private assessmentResults: Map<string, SecurityAssessmentResult> = new Map();
constructor() {
COMMON_THREATS.forEach(threat => {
this.threats.set(threat.id, threat);
});
}
async assessThreat(threatId: string, context: any): Promise<SecurityAssessmentResult> {
const threat = this.threats.get(threatId);
if (!threat) {
throw new Error(`Unknown threat: ${threatId}`);
}
const result: SecurityAssessmentResult = {
threatId,
assessedAt: new Date(),
riskLevel: this.calculateRiskLevel(threat, context),
mitigationsApplied: [],
recommendations: [...threat.mitigations],
context,
};
this.assessmentResults.set(threatId, result);
return result;
}
private calculateRiskLevel(threat: SecurityThreat, context: any): 'low' | 'medium' | 'high' | 'critical' {
// Simplified risk calculation logic
let riskLevel = threat.severity;
// Adjust risk level based on context
if (context.hasPublicAccess && riskLevel !== 'critical') {
riskLevel = this.increaseSeverity(riskLevel);
}
if (context.hasPrivilegedOperations && riskLevel !== 'critical') {
riskLevel = this.increaseSeverity(riskLevel);
}
return riskLevel;
}
private increaseSeverity(current: string): 'low' | 'medium' | 'high' | 'critical' {
switch (current) {
case 'low': return 'medium';
case 'medium': return 'high';
case 'high': return 'critical';
default: return 'critical';
}
}
}
export interface SecurityAssessmentResult {
threatId: string;
assessedAt: Date;
riskLevel: 'low' | 'medium' | 'high' | 'critical';
mitigationsApplied: string[];
recommendations: string[];
context: any;
}
9.1.2 Security Configuration and Policies
// src/security/SecurityConfig.ts
export interface SecurityConfig {
authentication: {
required: boolean;
methods: AuthMethod[];
sessionTimeout: number; // milliseconds
maxLoginAttempts: number;
lockoutDuration: number; // milliseconds
};
authorization: {
model: 'rbac' | 'abac' | 'none';
defaultRole?: string;
requireExplicitPermissions: boolean;
};
inputValidation: {
enableStrictValidation: boolean;
maxPayloadSize: number; // bytes
allowedContentTypes: string[];
sanitizeInputs: boolean;
};
rateLimiting: {
enabled: boolean;
windowMs: number;
maxRequests: number;
skipSuccessfulRequests?: boolean;
};
encryption: {
algorithm: string;
keySize: number;
encryptSensitiveData: boolean;
};
audit: {
enabled: boolean;
logLevel: 'minimal' | 'standard' | 'detailed';
retentionDays: number;
};
cors: {
enabled: boolean;
allowedOrigins: string[];
allowedMethods: string[];
allowedHeaders: string[];
credentials: boolean;
};
}
export const DEFAULT_SECURITY_CONFIG: SecurityConfig = {
authentication: {
required: true,
methods: [AuthMethod.API_KEY],
sessionTimeout: 3600000, // 1 hour
maxLoginAttempts: 5,
lockoutDuration: 900000, // 15 minutes
},
authorization: {
model: 'rbac',
defaultRole: 'user',
requireExplicitPermissions: true,
},
inputValidation: {
enableStrictValidation: true,
maxPayloadSize: 1024 * 1024, // 1MB
allowedContentTypes: ['application/json'],
sanitizeInputs: true,
},
rateLimiting: {
enabled: true,
windowMs: 60000, // 1 minute
maxRequests: 100,
skipSuccessfulRequests: false,
},
encryption: {
algorithm: 'aes-256-gcm',
keySize: 32,
encryptSensitiveData: true,
},
audit: {
enabled: true,
logLevel: 'standard',
retentionDays: 30,
},
cors: {
enabled: false,
allowedOrigins: [],
allowedMethods: ['POST'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: false,
},
};
export enum AuthMethod {
API_KEY = 'api_key',
JWT = 'jwt',
OAUTH2 = 'oauth2',
BASIC = 'basic',
}
9.2 Authentication System
9.2.1 Authentication Interface and Implementation
// src/auth/Authentication.ts
export interface AuthenticationProvider {
name: string;
authenticate(credentials: any): Promise<AuthenticationResult>;
validateToken(token: string): Promise<TokenValidationResult>;
refreshToken?(refreshToken: string): Promise<RefreshTokenResult>;
}
export interface AuthenticationResult {
success: boolean;
user?: UserInfo;
token?: string;
refreshToken?: string;
expiresAt?: Date;
error?: string;
}
export interface TokenValidationResult {
valid: boolean;
user?: UserInfo;
expiresAt?: Date;
error?: string;
}
export interface RefreshTokenResult {
success: boolean;
token?: string;
refreshToken?: string;
expiresAt?: Date;
error?: string;
}
export interface UserInfo {
id: string;
username: string;
email?: string;
roles: string[];
permissions: string[];
metadata?: Record<string, any>;
}
// API Key Authentication Provider
export class ApiKeyAuthProvider implements AuthenticationProvider {
name = 'api_key';
private apiKeys = new Map<string, UserInfo>();
private hashedKeys = new Map<string, string>(); // hashed -> original
constructor(private secretKey: string) {}
async authenticate(credentials: { apiKey: string }): Promise<AuthenticationResult> {
if (!credentials.apiKey) {
return {
success: false,
error: 'API key is required',
};
}
const hashedKey = await this.hashApiKey(credentials.apiKey);
const user = this.apiKeys.get(hashedKey);
if (!user) {
return {
success: false,
error: 'Invalid API key',
};
}
return {
success: true,
user,
token: credentials.apiKey, // API key serves as token
};
}
async validateToken(token: string): Promise<TokenValidationResult> {
const hashedKey = await this.hashApiKey(token);
const user = this.apiKeys.get(hashedKey);
return {
valid: !!user,
user,
error: user ? undefined : 'Invalid token',
};
}
async addApiKey(apiKey: string, user: UserInfo): Promise<void> {
const hashedKey = await this.hashApiKey(apiKey);
this.apiKeys.set(hashedKey, user);
this.hashedKeys.set(hashedKey, apiKey);
}
async revokeApiKey(apiKey: string): Promise<boolean> {
const hashedKey = await this.hashApiKey(apiKey);
const existed = this.apiKeys.has(hashedKey);
this.apiKeys.delete(hashedKey);
this.hashedKeys.delete(hashedKey);
return existed;
}
private async hashApiKey(apiKey: string): Promise<string> {
const crypto = await import('crypto');
return crypto
.createHmac('sha256', this.secretKey)
.update(apiKey)
.digest('hex');
}
}
// JWT Authentication Provider
export class JWTAuthProvider implements AuthenticationProvider {
name = 'jwt';
constructor(
private secretKey: string,
private issuer: string = 'mcp-server',
private expirationTime: string = '1h'
) {}
async authenticate(credentials: { username: string; password: string }): Promise<AuthenticationResult> {
// Here you should validate the username and password
const user = await this.validateUserCredentials(credentials.username, credentials.password);
if (!user) {
return {
success: false,
error: 'Invalid credentials',
};
}
const jwt = await import('jsonwebtoken');
const token = jwt.sign(
{
sub: user.id,
username: user.username,
roles: user.roles,
permissions: user.permissions,
},
this.secretKey,
{
issuer: this.issuer,
expiresIn: this.expirationTime,
}
);
return {
success: true,
user,
token,
expiresAt: new Date(Date.now() + 3600000), // 1 hour
};
}
async validateToken(token: string): Promise<TokenValidationResult> {
try {
const jwt = await import('jsonwebtoken');
const decoded = jwt.verify(token, this.secretKey) as any;
const user: UserInfo = {
id: decoded.sub,
username: decoded.username,
roles: decoded.roles || [],
permissions: decoded.permissions || [],
};
return {
valid: true,
user,
expiresAt: new Date(decoded.exp * 1000),
};
} catch (error) {
return {
valid: false,
error: error instanceof Error ? error.message : 'Token validation failed',
};
}
}
async refreshToken(refreshToken: string): Promise<RefreshTokenResult> {
try {
const jwt = await import('jsonwebtoken');
const decoded = jwt.verify(refreshToken, this.secretKey) as any;
// Generate a new access token
const newToken = jwt.sign(
{
sub: decoded.sub,
username: decoded.username,
roles: decoded.roles,
permissions: decoded.permissions,
},
this.secretKey,
{
issuer: this.issuer,
expiresIn: this.expirationTime,
}
);
return {
success: true,
token: newToken,
refreshToken, // Keep the same refresh token
expiresAt: new Date(Date.now() + 3600000),
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Refresh token validation failed',
};
}
}
private async validateUserCredentials(username: string, password: string): Promise<UserInfo | null> {
// Here you should query a database or other user store
// For this example, we return a mock user
if (username === 'admin' && password === 'password123') {
return {
id: 'admin-001',
username: 'admin',
email: 'admin@example.com',
roles: ['admin'],
permissions: ['*'],
};
}
return null;
}
}
// Authentication Manager
export class AuthenticationManager {
private providers = new Map<string, AuthenticationProvider>();
private defaultProvider?: string;
private logger: Logger;
constructor(logger: Logger) {
this.logger = logger.child({}, 'auth');
}
registerProvider(provider: AuthenticationProvider): void {
this.providers.set(provider.name, provider);
if (!this.defaultProvider) {
this.defaultProvider = provider.name;
}
this.logger.info(`Authentication provider registered: ${provider.name}`);
}
setDefaultProvider(name: string): void {
if (!this.providers.has(name)) {
throw new Error(`Provider not found: ${name}`);
}
this.defaultProvider = name;
}
async authenticate(
credentials: any,
providerName?: string
): Promise<AuthenticationResult> {
const provider = this.getProvider(providerName);
try {
const result = await provider.authenticate(credentials);
await this.logger.info(
`Authentication attempt: ${result.success ? 'success' : 'failure'}`,
{
provider: provider.name,
username: credentials.username || 'unknown',
success: result.success,
}
);
return result;
} catch (error) {
await this.logger.error(
'Authentication error',
error instanceof Error ? error : new Error(String(error)),
{ provider: provider.name }
);
return {
success: false,
error: 'Authentication system error',
};
}
}
async validateToken(
token: string,
providerName?: string
): Promise<TokenValidationResult> {
const provider = this.getProvider(providerName);
try {
return await provider.validateToken(token);
} catch (error) {
await this.logger.error(
'Token validation error',
error instanceof Error ? error : new Error(String(error)),
{ provider: provider.name }
);
return {
valid: false,
error: 'Token validation system error',
};
}
}
private getProvider(name?: string): AuthenticationProvider {
const providerName = name || this.defaultProvider;
if (!providerName) {
throw new Error('No authentication provider configured');
}
const provider = this.providers.get(providerName);
if (!provider) {
throw new Error(`Provider not found: ${providerName}`);
}
return provider;
}
}
9.3 Authorization and Access Control
9.3.1 Role-Based Access Control (RBAC)
// src/auth/Authorization.ts
export interface Permission {
id: string;
name: string;
description: string;
resource: string;
action: string;
conditions?: PermissionCondition[];
}
export interface PermissionCondition {
type: 'time' | 'ip' | 'resource_owner' | 'custom';
operator: 'equals' | 'in' | 'not_in' | 'greater_than' | 'less_than';
value: any;
}
export interface Role {
id: string;
name: string;
description: string;
permissions: string[]; // Permission IDs
inherits?: string[]; // Parent role IDs
}
export interface AuthorizationContext {
user: UserInfo;
resource: string;
action: string;
metadata?: Record<string, any>;
timestamp: Date;
clientInfo?: {
ip?: string;
userAgent?: string;
};
}
export interface AuthorizationResult {
allowed: boolean;
reason?: string;
matchedPermissions?: Permission[];
failedConditions?: PermissionCondition[];
}
export class RBACAuthorizationProvider {
private permissions = new Map<string, Permission>();
private roles = new Map<string, Role>();
private logger: Logger;
constructor(logger: Logger) {
this.logger = logger.child({}, 'rbac');
this.setupDefaultPermissions();
this.setupDefaultRoles();
}
// Permission management
addPermission(permission: Permission): void {
this.permissions.set(permission.id, permission);
this.logger.debug('Permission added', { permissionId: permission.id });
}
addRole(role: Role): void {
// Validate permission existence
for (const permissionId of role.permissions) {
if (!this.permissions.has(permissionId)) {
throw new Error(`Permission not found: ${permissionId}`);
}
}
// Validate inherited role existence
if (role.inherits) {
for (const parentRoleId of role.inherits) {
if (!this.roles.has(parentRoleId)) {
throw new Error(`Parent role not found: ${parentRoleId}`);
}
}
}
this.roles.set(role.id, role);
this.logger.debug('Role added', { roleId: role.id });
}
// Authorization check
async authorize(context: AuthorizationContext): Promise<AuthorizationResult> {
const userPermissions = await this.getUserPermissions(context.user);
// Check for matching permissions
const matchedPermissions: Permission[] = [];
const failedConditions: PermissionCondition[] = [];
for (const permission of userPermissions) {
if (this.matchesResourceAndAction(permission, context.resource, context.action)) {
// Check conditions
const conditionResult = await this.evaluateConditions(permission, context);
if (conditionResult.success) {
matchedPermissions.push(permission);
} else {
failedConditions.push(...(conditionResult.failedConditions || []));
}
}
}
const allowed = matchedPermissions.length > 0;
await this.logger.info(
`Authorization ${allowed ? 'granted' : 'denied'}`,
{
userId: context.user.id,
resource: context.resource,
action: context.action,
matchedPermissions: matchedPermissions.length,
reason: allowed ? 'permissions matched' : 'no matching permissions',
}
);
return {
allowed,
reason: allowed ? 'Access granted' : 'No matching permissions found',
matchedPermissions,
failedConditions: failedConditions.length > 0 ? failedConditions : undefined,
};
}
// Get all permissions for a user (including inherited)
private async getUserPermissions(user: UserInfo): Promise<Permission[]> {
const allPermissions = new Set<Permission>();
// Direct permissions
for (const permissionId of user.permissions) {
const permission = this.permissions.get(permissionId);
if (permission) {
allPermissions.add(permission);
}
}
// Role permissions
for (const roleId of user.roles) {
const rolePermissions = await this.getRolePermissions(roleId);
rolePermissions.forEach(p => allPermissions.add(p));
}
return Array.from(allPermissions);
}
// Get permissions for a role (recursively handles inheritance)
private async getRolePermissions(roleId: string, visited = new Set<string>()): Promise<Permission[]> {
if (visited.has(roleId)) {
this.logger.warn('Circular role inheritance detected', { roleId });
return [];
}
visited.add(roleId);
const role = this.roles.get(roleId);
if (!role) {
return [];
}
const permissions = new Set<Permission>();
// Direct permissions
for (const permissionId of role.permissions) {
const permission = this.permissions.get(permissionId);
if (permission) {
permissions.add(permission);
}
}
// Inherited permissions
if (role.inherits) {
for (const parentRoleId of role.inherits) {
const parentPermissions = await this.getRolePermissions(parentRoleId, new Set(visited));
parentPermissions.forEach(p => permissions.add(p));
}
}
return Array.from(permissions);
}
private matchesResourceAndAction(permission: Permission, resource: string, action: string): boolean {
// Supports wildcard matching
const resourceMatches = permission.resource === '*' ||
permission.resource === resource ||
resource.startsWith(permission.resource.replace('*', ''));
const actionMatches = permission.action === '*' ||
permission.action === action ||
action.startsWith(permission.action.replace('*', ''));
return resourceMatches && actionMatches;
}
private async evaluateConditions(
permission: Permission,
context: AuthorizationContext
): Promise<{ success: boolean; failedConditions?: PermissionCondition[] }> {
if (!permission.conditions || permission.conditions.length === 0) {
return { success: true };
}
const failedConditions: PermissionCondition[] = [];
for (const condition of permission.conditions) {
const result = await this.evaluateCondition(condition, context);
if (!result) {
failedConditions.push(condition);
}
}
return {
success: failedConditions.length === 0,
failedConditions: failedConditions.length > 0 ? failedConditions : undefined,
};
}
private async evaluateCondition(condition: PermissionCondition, context: AuthorizationContext): Promise<boolean> {
switch (condition.type) {
case 'time':
return this.evaluateTimeCondition(condition, context);
case 'ip':
return this.evaluateIPCondition(condition, context);
case 'resource_owner':
return this.evaluateResourceOwnerCondition(condition, context);
case 'custom':
return this.evaluateCustomCondition(condition, context);
default:
this.logger.warn('Unknown condition type', { type: condition.type });
return false;
}
}
private evaluateTimeCondition(condition: PermissionCondition, context: AuthorizationContext): boolean {
const currentHour = context.timestamp.getHours();
switch (condition.operator) {
case 'greater_than':
return currentHour > condition.value;
case 'less_than':
return currentHour < condition.value;
case 'in':
return Array.isArray(condition.value) && condition.value.includes(currentHour);
default:
return false;
}
}
private evaluateIPCondition(condition: PermissionCondition, context: AuthorizationContext): boolean {
const clientIP = context.clientInfo?.ip;
if (!clientIP) return false;
switch (condition.operator) {
case 'equals':
return clientIP === condition.value;
case 'in':
return Array.isArray(condition.value) && condition.value.includes(clientIP);
case 'not_in':
return Array.isArray(condition.value) && !condition.value.includes(clientIP);
default:
return false;
}
}
private evaluateResourceOwnerCondition(condition: PermissionCondition, context: AuthorizationContext): boolean {
const resourceOwner = context.metadata?.owner;
return resourceOwner === context.user.id;
}
private evaluateCustomCondition(condition: PermissionCondition, context: AuthorizationContext): boolean {
// Custom condition evaluation logic can be implemented here
return false;
}
private setupDefaultPermissions(): void {
// Tool permissions
this.addPermission({
id: 'tools.list',
name: 'List Tools',
description: 'View the list of available tools',
resource: 'tools',
action: 'list',
});
this.addPermission({
id: 'tools.execute',
name: 'Execute Tools',
description: 'Execute tools',
resource: 'tools',
action: 'execute',
});
// Resource permissions
this.addPermission({
id: 'resources.read',
name: 'Read Resources',
description: 'Read resources',
resource: 'resources',
action: 'read',
});
this.addPermission({
id: 'resources.write',
name: 'Write Resources',
description: 'Write to resources',
resource: 'resources',
action: 'write',
conditions: [
{
type: 'resource_owner',
operator: 'equals',
value: true,
},
],
});
// Admin permissions
this.addPermission({
id: 'admin.all',
name: 'Admin All',
description: 'Administrator permissions',
resource: '*',
action: '*',
});
}
private setupDefaultRoles(): void {
this.addRole({
id: 'guest',
name: 'Guest',
description: 'Guest role',
permissions: ['tools.list'],
});
this.addRole({
id: 'user',
name: 'User',
description: 'Regular user role',
permissions: ['tools.list', 'tools.execute', 'resources.read'],
inherits: ['guest'],
});
this.addRole({
id: 'admin',
name: 'Administrator',
description: 'Administrator role',
permissions: ['admin.all'],
});
}
}
9.4 Input Validation and Data Sanitization
9.4.1 Input Validation System
// src/validation/InputValidator.ts
import { z } from 'zod';
export interface ValidationRule {
name: string;
schema: z.ZodSchema;
sanitizer?: (input: any) => any;
}
export interface ValidationResult {
valid: boolean;
data?: any;
errors?: ValidationError[];
sanitizedData?: any;
}
export interface ValidationError {
field: string;
message: string;
code: string;
value?: any;
}
export class InputValidator {
private rules = new Map<string, ValidationRule>();
private logger: Logger;
constructor(logger: Logger) {
this.logger = logger.child({}, 'validation');
this.setupDefaultRules();
}
addRule(name: string, rule: ValidationRule): void {
this.rules.set(name, rule);
this.logger.debug('Validation rule added', { name });
}
async validate(ruleName: string, input: any): Promise<ValidationResult> {
const rule = this.rules.get(ruleName);
if (!rule) {
return {
valid: false,
errors: [{
field: 'rule',
message: `Validation rule not found: ${ruleName}`,
code: 'RULE_NOT_FOUND',
}],
};
}
try {
// First, sanitize the data
const sanitizedData = rule.sanitizer ? rule.sanitizer(input) : input;
// Then, perform validation
const validatedData = rule.schema.parse(sanitizedData);
return {
valid: true,
data: validatedData,
sanitizedData,
};
} catch (error) {
if (error instanceof z.ZodError) {
const validationErrors: ValidationError[] = error.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
code: e.code,
value: e.input,
}));
await this.logger.warn('Validation failed',
{
rule: ruleName,
errors: validationErrors.length,
input: this.sanitizeLogInput(input),
}
);
return {
valid: false,
errors: validationErrors,
};
}
await this.logger.error(
'Validation error',
error instanceof Error ? error : new Error(String(error)),
{ rule: ruleName }
);
return {
valid: false,
errors: [{
field: 'validation',
message: 'Validation system error',
code: 'SYSTEM_ERROR',
}],
};
}
}
private setupDefaultRules(): void {
// MCP request validation
this.addRule('mcp-request', {
name: 'MCP Request',
schema: z.object({
jsonrpc: z.string().refine(v => v === '2.0'),
id: z.union([z.string(), z.number()]).optional(),
method: z.string().min(1).max(100),
params: z.record(z.unknown()).optional(),
}),
sanitizer: (input) => ({
...input,
method: typeof input?.method === 'string' ? input.method.trim() : input?.method,
}),
});
// Tool execution parameter validation
this.addRule('tool-execution', {
name: 'Tool Execution',
schema: z.object({
name: z.string().min(1).max(100).regex(/^[a-zA-Z0-9_-]+$/),
arguments: z.record(z.unknown()).optional(),
}),
sanitizer: (input) => ({
...input,
name: typeof input?.name === 'string' ? input.name.trim() : input?.name,
}),
});
// Resource URI validation
this.addRule('resource-uri', {
name: 'Resource URI',
schema: z.string().min(1).max(2048).refine(uri => {
try {
new URL(uri);
return true;
} catch {
return uri.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:.+$/);
}
}, 'Must be a valid URI'),
sanitizer: (input) => typeof input === 'string' ? input.trim() : input,
});
// File path validation
this.addRule('file-path', {
name: 'File Path',
schema: z.string().min(1).max(4096).refine(path => {
// Prevent path traversal attacks
const normalizedPath = path.replace(/\\/g, '/');
return !normalizedPath.includes('../') &&
!normalizedPath.includes('./') &&
!normalizedPath.startsWith('/') &&
!normalizedPath.includes('//');
}, 'Invalid file path'),
sanitizer: (input) => {
if (typeof input !== 'string') return input;
// Normalize path separators and remove dangerous characters
return input.replace(/\\/g, '/').replace(/[<>:"|?*]/g, '');
},
});
}
private sanitizeLogInput(input: any): any {
// Avoid logging sensitive information
if (typeof input === 'object' && input !== null) {
const sanitized = { ...input };
const sensitiveFields = ['password', 'token', 'apiKey', 'secret'];
for (const field of sensitiveFields) {
if (field in sanitized) {
sanitized[field] = '[REDACTED]';
}
}
return sanitized;
}
return input;
}
}
// Data Sanitization Utilities
export class DataSanitizer {
static sanitizeHtml(input: string): string {
// Simple HTML sanitization, use a dedicated library like DOMPurify in a real application
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
}
static sanitizeSQL(input: string): string {
// SQL injection protection
return input.replace(/['";\\]/g, '\\$&');
}
static sanitizeCommandLine(input: string): string {
// Command line injection protection
return input.replace(/[;&|`$(){}[<>*?~]/g, '\\$&');
}
static sanitizeFileName(input: string): string {
// Filename sanitization
return input.replace(/[^a-zA-Z0-9._-]/g, '_').substring(0, 255);
}
static limitString(input: string, maxLength: number = 1000): string {
return input.length > maxLength ? input.substring(0, maxLength) + '...' : input;
}
static removeNullBytes(input: string): string {
return input.replace(/\0/g, '');
}
}
9.5 Rate Limiting and DDoS Protection
9.5.1 Request Rate Limiting
// src/security/RateLimiter.ts
export interface RateLimitConfig {
windowMs: number; // Time window in milliseconds
maxRequests: number; // Maximum number of requests
skipSuccessfulRequests?: boolean; // Skip successful requests
skipFailedRequests?: boolean; // Skip failed requests
keyGenerator?: (context: any) => string; // Key generator
}
export interface RateLimitResult {
allowed: boolean;
remaining: number;
resetTime: Date;
retryAfter?: number; // seconds
}
export class RateLimiter {
private requests = new Map<string, RequestRecord[]>();
private logger: Logger;
constructor(
private config: RateLimitConfig,
logger: Logger
) {
this.logger = logger.child({}, 'rate-limiter');
// Periodically clean up expired records
setInterval(() => {
this.cleanExpiredRecords();
}, Math.min(this.config.windowMs, 60000)); // Clean at most once per minute
}
async checkLimit(context: any): Promise<RateLimitResult> {
const key = this.config.keyGenerator ? this.config.keyGenerator(context) : 'default';
const now = Date.now();
const windowStart = now - this.config.windowMs;
// Get or create request records
let records = this.requests.get(key) || [];
// Clean up expired records
records = records.filter(record => record.timestamp > windowStart);
// Calculate remaining quota
const remaining = Math.max(0, this.config.maxRequests - records.length);
const resetTime = new Date(now + this.config.windowMs);
const result: RateLimitResult = {
allowed: remaining > 0,
remaining: remaining > 0 ? remaining - 1 : 0,
resetTime,
};
if (!result.allowed) {
// Calculate retry time
const oldestRecord = records[0];
if (oldestRecord) {
result.retryAfter = Math.ceil((oldestRecord.timestamp + this.config.windowMs - now) / 1000);
}
await this.logger.warn('Rate limit exceeded',
{
key,
requests: records.length,
limit: this.config.maxRequests,
windowMs: this.config.windowMs,
}
);
}
// If the request is allowed, record it
if (result.allowed) {
records.push({
timestamp: now,
success: undefined, // Will be updated after the request is completed
});
this.requests.set(key, records);
}
return result;
}
markRequestComplete(context: any, success: boolean): void {
if (
(success && this.config.skipSuccessfulRequests) ||
(!success && this.config.skipFailedRequests)
) {
const key = this.config.keyGenerator ? this.config.keyGenerator(context) : 'default';
const records = this.requests.get(key);
if (records && records.length > 0) {
// Remove the last record
records.pop();
if (records.length === 0) {
this.requests.delete(key);
}
}
}
}
private cleanExpiredRecords(): void {
const now = Date.now();
const windowStart = now - this.config.windowMs;
for (const [key, records] of this.requests) {
const validRecords = records.filter(record => record.timestamp > windowStart);
if (validRecords.length === 0) {
this.requests.delete(key);
} else if (validRecords.length < records.length) {
this.requests.set(key, validRecords);
}
}
}
getStats(): { totalKeys: number; totalRequests: number } {
let totalRequests = 0;
for (const records of this.requests.values()) {
totalRequests += records.length;
}
return {
totalKeys: this.requests.size,
totalRequests,
};
}
reset(key?: string): void {
if (key) {
this.requests.delete(key);
this.logger.info('Rate limit reset for key', { key });
} else {
this.requests.clear();
this.logger.info('All rate limits reset');
}
}
}
interface RequestRecord {
timestamp: number;
success?: boolean;
}
// Multi-tier Rate Limiter
export class MultiTierRateLimiter {
private limiters = new Map<string, RateLimiter>();
private logger: Logger;
constructor(logger: Logger) {
this.logger = logger.child({}, 'multi-tier-limiter');
}
addTier(name: string, config: RateLimitConfig): void {
this.limiters.set(name, new RateLimiter(config, this.logger));
this.logger.info('Rate limit tier added', { name, config });
}
async checkAllTiers(context: any): Promise<{ allowed: boolean; tier?: string; result?: RateLimitResult }> {
for (const [tierName, limiter] of this.limiters) {
const result = await limiter.checkLimit(context);
if (!result.allowed) {
return {
allowed: false,
tier: tierName,
result,
};
}
}
return { allowed: true };
}
markComplete(context: any, success: boolean): void {
for (const limiter of this.limiters.values()) {
limiter.markRequestComplete(context, success);
}
}
}
// Adaptive Rate Limiter
export class AdaptiveRateLimiter extends RateLimiter {
private successRate = 1.0;
private errorRate = 0.0;
private adaptiveMultiplier = 1.0;
private minMultiplier = 0.1;
private maxMultiplier = 2.0;
private adaptationWindow = 60000; // 1 minute
constructor(config: RateLimitConfig, logger: Logger) {
super(config, logger);
// Periodically adjust the rate limit
setInterval(() => {
this.adjustRateLimit();
}, this.adaptationWindow);
}
async checkLimit(context: any): Promise<RateLimitResult> {
const baseResult = await super.checkLimit(context);
// Apply adaptive adjustment
const adjustedMaxRequests = Math.floor(this.config.maxRequests * this.adaptiveMultiplier);
const adjustedRemaining = Math.floor(baseResult.remaining * this.adaptiveMultiplier);
return {
...baseResult,
allowed: adjustedRemaining > 0,
remaining: Math.max(0, adjustedRemaining),
};
}
recordResult(success: boolean): void {
// Update success rate statistics
const alpha = 0.1; // Exponential moving average coefficient
if (success) {
this.successRate = alpha + (1 - alpha) * this.successRate;
this.errorRate = (1 - alpha) * this.errorRate;
} else {
this.errorRate = alpha + (1 - alpha) * this.errorRate;
this.successRate = (1 - alpha) * this.successRate;
}
}
private adjustRateLimit(): void {
const previousMultiplier = this.adaptiveMultiplier;
// Adjust rate limit based on error rate
if (this.errorRate > 0.1) {
// High error rate, reduce rate limit
this.adaptiveMultiplier *= 0.8;
} else if (this.errorRate < 0.01) {
// Low error rate, increase rate limit
this.adaptiveMultiplier *= 1.1;
}
// Limit adjustment range
this.adaptiveMultiplier = Math.max(this.minMultiplier,
Math.min(this.maxMultiplier, this.adaptiveMultiplier));
if (Math.abs(this.adaptiveMultiplier - previousMultiplier) > 0.01) {
this.logger.info('Rate limit adjusted',
{
previousMultiplier,
newMultiplier: this.adaptiveMultiplier,
successRate: this.successRate,
errorRate: this.errorRate,
}
);
}
}
}
9.6 Security Auditing and Monitoring
9.6.1 Security Event Monitoring
// src/security/SecurityMonitor.ts
export enum SecurityEventType {
AUTHENTICATION_FAILURE = 'auth_failure',
AUTHORIZATION_DENIED = 'auth_denied',
SUSPICIOUS_ACTIVITY = 'suspicious_activity',
RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded',
INPUT_VALIDATION_FAILED = 'validation_failed',
PRIVILEGE_ESCALATION = 'privilege_escalation',
DATA_ACCESS_VIOLATION = 'data_violation',
SYSTEM_INTRUSION = 'intrusion',
}
export interface SecurityEvent {
id: string;
type: SecurityEventType;
severity: 'low' | 'medium' | 'high' | 'critical';
timestamp: Date;
userId?: string;
clientInfo?: {
ip?: string;
userAgent?: string;
location?: string;
};
resource?: string;
action?: string;
details: Record<string, any>;
metadata?: Record<string, any>;
}
export interface SecurityAlert {
id: string;
event: SecurityEvent;
threshold: SecurityThreshold;
count: number;
firstOccurrence: Date;
lastOccurrence: Date;
status: 'active' | 'resolved' | 'suppressed';
}
export interface SecurityThreshold {
eventType: SecurityEventType;
count: number;
windowMs: number;
severity: 'low' | 'medium' | 'high' | 'critical';
actions: SecurityAction[];
}
export enum SecurityAction {
LOG_ALERT = 'log_alert',
SEND_EMAIL = 'send_email',
BLOCK_USER = 'block_user',
BLOCK_IP = 'block_ip',
RATE_LIMIT = 'rate_limit',
REQUIRE_MFA = 'require_mfa',
}
export class SecurityMonitor {
private events: SecurityEvent[] = [];
private alerts = new Map<string, SecurityAlert>();
private thresholds: SecurityThreshold[] = [];
private eventCounters = new Map<string, { count: number; windowStart: number }>();
private logger: Logger;
private maxEvents = 10000;
constructor(logger: Logger) {
this.logger = logger.child({}, 'security-monitor');
this.setupDefaultThresholds();
// Periodically clean up old events
setInterval(() => {
this.cleanupOldEvents();
}, 300000); // Every 5 minutes
}
async recordEvent(event: Omit<SecurityEvent, 'id' | 'timestamp'>): Promise<void> {
const securityEvent: SecurityEvent = {
...event,
id: this.generateEventId(),
timestamp: new Date(),
};
// Record event
this.events.push(securityEvent);
// Limit number of events
if (this.events.length > this.maxEvents) {
this.events.shift();
}
// Log event
await this.logger.warn(
`Security event: ${event.type}`,
{
eventId: securityEvent.id,
type: event.type,
severity: event.severity,
userId: event.userId,
resource: event.resource,
action: event.action,
details: event.details,
}
);
// Check if a threshold has been triggered
await this.checkThresholds(securityEvent);
}
private async checkThresholds(event: SecurityEvent): Promise<void> {
for (const threshold of this.thresholds) {
if (threshold.eventType === event.type) {
await this.evaluateThreshold(threshold, event);
}
}
}
private async evaluateThreshold(threshold: SecurityThreshold, event: SecurityEvent): Promise<void> {
const key = `${threshold.eventType}:${event.userId || 'anonymous'}:${event.clientInfo?.ip || 'unknown'}`;
const now = Date.now();
const windowStart = now - threshold.windowMs;
// Clean up expired counts
const counter = this.eventCounters.get(key);
if (!counter || counter.windowStart < windowStart) {
this.eventCounters.set(key, { count: 1, windowStart: now });
return;
}
// Increment count
counter.count++;
// Check if the threshold has been exceeded
if (counter.count >= threshold.count) {
await this.triggerAlert(threshold, event, counter.count);
}
}
private async triggerAlert(threshold: SecurityThreshold, event: SecurityEvent, count: number): Promise<void> {
const alertId = this.generateAlertId();
const alert: SecurityAlert = {
id: alertId,
event,
threshold,
count,
firstOccurrence: new Date(Date.now() - threshold.windowMs), // Approximate first occurrence
lastOccurrence: event.timestamp,
status: 'active',
};
this.alerts.set(alertId, alert);
await this.logger.error(
`Security alert triggered: ${threshold.eventType}`,
{
alertId,
eventType: threshold.eventType,
count,
threshold: threshold.count,
windowMs: threshold.windowMs,
severity: threshold.severity,
}
);
// Execute security actions
await this.executeSecurityActions(threshold, event);
}
private async executeSecurityActions(threshold: SecurityThreshold, event: SecurityEvent): Promise<void> {
for (const action of threshold.actions) {
try {
await this.executeSecurityAction(action, threshold, event);
} catch (error) {
await this.logger.error(
'Security action execution failed',
error instanceof Error ? error : new Error(String(error)),
{ action, eventType: threshold.eventType }
);
}
}
}
private async executeSecurityAction(action: SecurityAction, threshold: SecurityThreshold, event: SecurityEvent): Promise<void> {
switch (action) {
case SecurityAction.LOG_ALERT:
await this.logger.error(
`SECURITY ALERT: ${threshold.eventType} threshold exceeded`,
{
severity: threshold.severity,
eventId: event.id,
userId: event.userId,
ip: event.clientInfo?.ip,
}
);
break;
case SecurityAction.BLOCK_USER:
if (event.userId) {
// Should call a user management system in a real implementation
await this.logger.warn('User blocking requested', { userId: event.userId });
}
break;
case SecurityAction.BLOCK_IP:
if (event.clientInfo?.ip) {
// Should call a firewall or network layer in a real implementation
await this.logger.warn('IP blocking requested', { ip: event.clientInfo.ip });
}
break;
case SecurityAction.SEND_EMAIL:
// Should send an email notification in a real implementation
await this.logger.info('Email notification requested', { eventType: threshold.eventType });
break;
default:
await this.logger.warn('Unknown security action', { action });
}
}
getEvents(filter?: {
type?: SecurityEventType;
severity?: string;
userId?: string;
since?: Date;
limit?: number;
}): SecurityEvent[] {
let filtered = this.events;
if (filter) {
if (filter.type) {
filtered = filtered.filter(e => e.type === filter.type);
}
if (filter.severity) {
filtered = filtered.filter(e => e.severity === filter.severity);
}
if (filter.userId) {
filtered = filtered.filter(e => e.userId === filter.userId);
}
if (filter.since) {
filtered = filtered.filter(e => e.timestamp >= filter.since!);
}
}
// Sort in reverse chronological order
filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
// Apply limit
if (filter?.limit) {
filtered = filtered.slice(0, filter.limit);
}
return filtered;
}
getActiveAlerts(): SecurityAlert[] {
return Array.from(this.alerts.values())
.filter(alert => alert.status === 'active')
.sort((a, b) => b.lastOccurrence.getTime() - a.lastOccurrence.getTime());
}
resolveAlert(alertId: string): boolean {
const alert = this.alerts.get(alertId);
if (alert) {
alert.status = 'resolved';
return true;
}
return false;
}
private setupDefaultThresholds(): void {
this.thresholds = [
{
eventType: SecurityEventType.AUTHENTICATION_FAILURE,
count: 5,
windowMs: 300000, // 5 minutes
severity: 'medium',
actions: [SecurityAction.LOG_ALERT, SecurityAction.RATE_LIMIT],
},
{
eventType: SecurityEventType.AUTHORIZATION_DENIED,
count: 10,
windowMs: 600000, // 10 minutes
severity: 'medium',
actions: [SecurityAction.LOG_ALERT],
},
{
eventType: SecurityEventType.RATE_LIMIT_EXCEEDED,
count: 3,
windowMs: 60000, // 1 minute
severity: 'low',
actions: [SecurityAction.LOG_ALERT],
},
{
eventType: SecurityEventType.PRIVILEGE_ESCALATION,
count: 1,
windowMs: 3600000, // 1 hour
severity: 'critical',
actions: [SecurityAction.LOG_ALERT, SecurityAction.SEND_EMAIL, SecurityAction.BLOCK_USER],
},
];
}
private generateEventId(): string {
return `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private generateAlertId(): string {
return `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private cleanupOldEvents(): void {
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
this.events = this.events.filter(event => event.timestamp > cutoff);
// Clean up expired counters
const now = Date.now();
for (const [key, counter] of this.eventCounters) {
if (counter.windowStart < now - 3600000) { // 1 hour ago
this.eventCounters.delete(key);
}
}
}
}
9.7 Integration Example: Secure MCP Server
// src/SecureMCPServer.ts
export class SecureMCPServer extends EnhancedMCPServer {
private authManager: AuthenticationManager;
private rbacProvider: RBACAuthorizationProvider;
private inputValidator: InputValidator;
private rateLimiter: MultiTierRateLimiter;
private securityMonitor: SecurityMonitor;
constructor(config: ServerConfig & { security: SecurityConfig }) {
super(config);
this.setupSecurity(config.security);
}
private setupSecurity(securityConfig: SecurityConfig): void {
// Authentication management
this.authManager = new AuthenticationManager(this.logger);
if (securityConfig.authentication.methods.includes(AuthMethod.API_KEY)) {
const apiKeyProvider = new ApiKeyAuthProvider(process.env.API_KEY_SECRET || 'default-secret');
this.authManager.registerProvider(apiKeyProvider);
}
if (securityConfig.authentication.methods.includes(AuthMethod.JWT)) {
const jwtProvider = new JWTAuthProvider(process.env.JWT_SECRET || 'default-secret');
this.authManager.registerProvider(jwtProvider);
}
// Authorization management
this.rbacProvider = new RBACAuthorizationProvider(this.logger);
// Input validation
this.inputValidator = new InputValidator(this.logger);
// Rate limiting
this.rateLimiter = new MultiTierRateLimiter(this.logger);
this.setupRateLimiting(securityConfig.rateLimiting);
// Security monitoring
this.securityMonitor = new SecurityMonitor(this.logger);
}
private setupRateLimiting(config: any): void {
if (!config.enabled) return;
// Global rate limiting
this.rateLimiter.addTier('global', {
windowMs: config.windowMs,
maxRequests: config.maxRequests,
keyGenerator: () => 'global',
});
// User-level rate limiting
this.rateLimiter.addTier('user', {
windowMs: config.windowMs,
maxRequests: Math.floor(config.maxRequests / 2),
keyGenerator: (context) => `user:${context.userId || 'anonymous'}`,
});
// IP-level rate limiting
this.rateLimiter.addTier('ip', {
windowMs: config.windowMs,
maxRequests: Math.floor(config.maxRequests * 2),
keyGenerator: (context) => `ip:${context.clientIP || 'unknown'}`,
});
}
// Enhanced request handling method
async handleSecureRequest(request: Request, context: {
clientIP?: string;
userAgent?: string;
authHeader?: string;
}): Promise<Response> {
const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const startTime = new Date();
try {
// 1. Input validation
const validationResult = await this.inputValidator.validate('mcp-request', request);
if (!validationResult.valid) {
await this.securityMonitor.recordEvent({
type: SecurityEventType.INPUT_VALIDATION_FAILED,
severity: 'medium',
clientInfo: { ip: context.clientIP, userAgent: context.userAgent },
details: { errors: validationResult.errors },
});
return {
jsonrpc: '2.0',
id: request.id,
error: MCPErrorBuilder.invalidParams('Input validation failed', validationResult.errors),
};
}
// 2. Authentication
let user: UserInfo | undefined;
if (context.authHeader) {
const authResult = await this.authManager.validateToken(context.authHeader);
if (!authResult.valid) {
await this.securityMonitor.recordEvent({
type: SecurityEventType.AUTHENTICATION_FAILURE,
severity: 'medium',
clientInfo: { ip: context.clientIP, userAgent: context.userAgent },
details: { error: authResult.error },
});
return {
jsonrpc: '2.0',
id: request.id,
error: {
code: MCPErrorCode.AUTHENTICATION_FAILED,
message: 'Authentication failed',
},
};
}
user = authResult.user;
}
// 3. Rate limit check
const rateLimitContext = {
userId: user?.id,
clientIP: context.clientIP,
};
const rateLimitResult = await this.rateLimiter.checkAllTiers(rateLimitContext);
if (!rateLimitResult.allowed) {
await this.securityMonitor.recordEvent({
type: SecurityEventType.RATE_LIMIT_EXCEEDED,
severity: 'low',
userId: user?.id,
clientInfo: { ip: context.clientIP },
details: { tier: rateLimitResult.tier },
});
return {
jsonrpc: '2.0',
id: request.id,
error: {
code: MCPErrorCode.RATE_LIMIT_EXCEEDED,
message: 'Rate limit exceeded',
data: {
retryAfter: rateLimitResult.result?.retryAfter,
resetTime: rateLimitResult.result?.resetTime,
},
},
};
}
// 4. Authorization check (if needed)
if (user && this.requiresAuthorization(request.method)) {
const authzContext: AuthorizationContext = {
user,
resource: this.getResourceFromMethod(request.method),
action: this.getActionFromMethod(request.method),
timestamp: startTime,
clientInfo: { ip: context.clientIP, userAgent: context.userAgent },
metadata: request.params,
};
const authzResult = await this.rbacProvider.authorize(authzContext);
if (!authzResult.allowed) {
await this.securityMonitor.recordEvent({
type: SecurityEventType.AUTHORIZATION_DENIED,
severity: 'medium',
userId: user.id,
resource: authzContext.resource,
action: authzContext.action,
clientInfo: { ip: context.clientIP },
details: { reason: authzResult.reason },
});
return {
jsonrpc: '2.0',
id: request.id,
error: {
code: MCPErrorCode.AUTHORIZATION_FAILED,
message: 'Authorization denied',
data: { reason: authzResult.reason },
},
};
}
}
// 5. Handle the request
const response = await this.handleRequest(request);
// 6. Mark rate limiting as complete
this.rateLimiter.markComplete(rateLimitContext, !response.error);
return response;
} catch (error) {
// Log system error
await this.securityMonitor.recordEvent({
type: SecurityEventType.SYSTEM_INTRUSION,
severity: 'high',
userId: user?.id,
clientInfo: { ip: context.clientIP },
details: { error: error instanceof Error ? error.message : String(error) },
});
this.rateLimiter.markComplete({ userId: user?.id, clientIP: context.clientIP }, false);
return {
jsonrpc: '2.0',
id: request.id,
error: {
code: MCPErrorCode.INTERNAL_ERROR,
message: 'Internal server error',
}