Skip to main content
Glama
token-security.ts12.6 kB
/** * Token Security Manager * * Comprehensive token detection and redaction system that prevents sensitive tokens * from being passed through the MCP server or logged, following MCP security best practices. */ import { logger } from './logger.js'; import { randomBytes } from 'crypto'; export class SecurityError extends Error { constructor(message: string) { super(message); this.name = 'SecurityError'; } } /** * Token patterns for detection and redaction */ const TOKEN_PATTERNS = { // API Keys apiKey: /(?:api[_-]?key|key)[=:\s]+([a-zA-Z0-9_-]{20,})/gi, // Bearer Tokens bearerToken: /Bearer\s+([a-zA-Z0-9_.-]{20,})/gi, // JWT Tokens jwtToken: /eyJ[a-zA-Z0-9_.-]+/gi, // OAuth Tokens accessToken: /(?:access[_-]?token)[=:\s]+([a-zA-Z0-9_-]{20,})/gi, refreshToken: /(?:refresh[_-]?token)[=:\s]+([a-zA-Z0-9_-]{20,})/gi, // Session Tokens sessionToken: /(?:session[_-]?token|sess[_-]?id)[=:\s]+([a-zA-Z0-9_-]{20,})/gi, // Database URLs with credentials mongoUrl: /(mongodb:\/\/[^:]+:[^@]+@[^\/]+)/gi, postgresUrl: /(postgres(?:ql)?:\/\/[^:]+:[^@]+@[^\/]+)/gi, mysqlUrl: /(mysql:\/\/[^:]+:[^@]+@[^\/]+)/gi, // AWS Credentials awsAccessKey: /AKIA[0-9A-Z]{16}/gi, awsSecretKey: /(?:aws[_-]?secret[_-]?access[_-]?key)[=:\s]+([a-zA-Z0-9\/+=]{40})/gi, // GitHub Tokens githubToken: /ghp_[a-zA-Z0-9]{36}/gi, githubTokenField: /(?:github[_-]?token)[=:\s]+([a-zA-Z0-9_-]{20,})/gi, // Generic secrets and passwords secret: /(?:secret|password|pwd|pass)[=:\s]+([a-zA-Z0-9_!@#$%^&*()-+=]{8,})/gi, // Authorization headers authHeader: /(?:authorization)[=:\s]+([a-zA-Z0-9_.-]{20,})/gi, }; /** * Sensitive field names that should be redacted */ const SENSITIVE_FIELDS = [ 'api_key', 'apiKey', 'api-key', 'token', 'access_token', 'accessToken', 'refresh_token', 'refreshToken', 'secret', 'password', 'pwd', 'pass', 'authorization', 'auth', 'credential', 'credentials', 'session_token', 'sessionToken', 'sess_id', 'sessionId', 'github_token', 'githubToken', 'aws_secret_access_key', 'awsSecretAccessKey', 'private_key', 'privateKey', 'client_secret', 'clientSecret' ]; /** * Session management interface */ interface SessionMetadata { [key: string]: any; } interface SessionEntry { id: string; metadata: SessionMetadata; createdAt: number; lastAccessedAt: number; expiresAt: number; } /** * Token Security Manager Class */ export class TokenSecurityManager { private static instance: TokenSecurityManager; private sessionStore = new Map<string, SessionEntry>(); private readonly sessionTimeout = 30 * 60 * 1000; // 30 minutes private readonly maxSessions = 1000; private readonly cleanupInterval = 5 * 60 * 1000; // 5 minutes private cleanupTimer?: NodeJS.Timeout; constructor() { this.startCleanupProcess(); logger.info('Token Security Manager initialized'); } /** * Get singleton instance */ static getInstance(): TokenSecurityManager { if (!TokenSecurityManager.instance) { TokenSecurityManager.instance = new TokenSecurityManager(); } return TokenSecurityManager.instance; } /** * Detect and redact sensitive tokens from any input */ redactSensitiveTokens(input: any): any { if (input === null || input === undefined) { return input; } // Handle strings if (typeof input === 'string') { return this.redactTokensFromString(input); } // Handle arrays if (Array.isArray(input)) { return input.map(item => this.redactSensitiveTokens(item)); } // Handle objects if (typeof input === 'object') { const redacted: any = {}; for (const [key, value] of Object.entries(input)) { // Check if field name is sensitive if (this.isSensitiveField(key)) { redacted[key] = '[REDACTED]'; logger.warn(`Sensitive field detected and redacted: ${key}`); } else { // Recursively redact nested objects/arrays redacted[key] = this.redactSensitiveTokens(value); } } return redacted; } return input; } /** * Validate that no tokens are being passed through */ validateNoTokenPassthrough(input: any, context: string): void { const tokens = this.detectTokens(input); if (tokens.length > 0) { logger.error(`Token passthrough attempt detected in ${context}: ${tokens.map(t => t.type).join(', ')}`); throw new SecurityError(`Token passthrough detected in ${context}. Sensitive data cannot be passed through the MCP server.`); } } /** * Detect tokens in input without redacting */ private detectTokens(input: any): Array<{ type: string; value: string }> { const tokens: Array<{ type: string; value: string }> = []; if (typeof input === 'string') { // Check string patterns for (const [type, pattern] of Object.entries(TOKEN_PATTERNS)) { const matches = input.match(pattern); if (matches) { matches.forEach(match => { tokens.push({ type, value: match }); }); } } } else if (typeof input === 'object' && input !== null) { // Check object fields for (const [key, value] of Object.entries(input)) { if (this.isSensitiveField(key) && typeof value === 'string' && value.length > 8) { tokens.push({ type: 'sensitive_field', value: key }); } // Recursively check nested objects if (typeof value === 'object' || Array.isArray(value)) { tokens.push(...this.detectTokens(value)); } } } else if (Array.isArray(input)) { // Check array elements input.forEach(item => { tokens.push(...this.detectTokens(item)); }); } return tokens; } /** * Redact tokens from a string */ private redactTokensFromString(text: string): string { let redacted = text; for (const [type, pattern] of Object.entries(TOKEN_PATTERNS)) { redacted = redacted.replace(pattern, (match, ...groups) => { logger.debug(`Token detected and redacted: ${type}`); // For Bearer tokens, keep the "Bearer " prefix if (type === 'bearerToken') { return 'Bearer [REDACTED]'; } // For field-based patterns, keep the field name if (match.includes('=') || match.includes(':')) { const separator = match.includes('=') ? '=' : ':'; const parts = match.split(separator); return `${parts[0]}${separator}[REDACTED]`; } // For standalone tokens, redact completely return '[REDACTED]'; }); } return redacted; } /** * Check if a field name is sensitive */ private isSensitiveField(fieldName: string): boolean { const lowerField = fieldName.toLowerCase(); return SENSITIVE_FIELDS.some(sensitive => lowerField.includes(sensitive.toLowerCase()) ); } /** * Create a new secure session */ createSession(metadata: SessionMetadata = {}): string { // Check session limits if (this.sessionStore.size >= this.maxSessions) { this.cleanupExpiredSessions(); if (this.sessionStore.size >= this.maxSessions) { throw new SecurityError('Maximum number of sessions reached'); } } // Generate secure session ID const sessionId = this.generateSecureSessionId(); const now = Date.now(); // Redact any sensitive data from metadata const redactedMetadata = this.redactSensitiveTokens(metadata); const session: SessionEntry = { id: sessionId, metadata: redactedMetadata, createdAt: now, lastAccessedAt: now, expiresAt: now + this.sessionTimeout }; this.sessionStore.set(sessionId, session); logger.debug(`Session created: ${sessionId.substring(0, 12)}...`); return sessionId; } /** * Validate and refresh a session */ validateSession(sessionId: string): boolean { const session = this.sessionStore.get(sessionId); if (!session) { return false; } const now = Date.now(); // Check if expired if (now > session.expiresAt) { this.sessionStore.delete(sessionId); logger.debug(`Session expired: ${sessionId.substring(0, 12)}...`); return false; } // Refresh session (sliding window) session.lastAccessedAt = now; session.expiresAt = now + this.sessionTimeout; logger.debug(`Session validated and refreshed: ${sessionId.substring(0, 12)}...`); return true; } /** * Get session metadata */ getSessionMetadata(sessionId: string): SessionMetadata | null { const session = this.sessionStore.get(sessionId); if (!session || !this.validateSession(sessionId)) { return null; } return { ...session.metadata }; } /** * Update session metadata */ updateSessionMetadata(sessionId: string, metadata: SessionMetadata): boolean { const session = this.sessionStore.get(sessionId); if (!session || !this.validateSession(sessionId)) { return false; } // Redact sensitive data from new metadata const redactedMetadata = this.redactSensitiveTokens(metadata); session.metadata = { ...session.metadata, ...redactedMetadata }; session.lastAccessedAt = Date.now(); logger.debug(`Session metadata updated: ${sessionId.substring(0, 12)}...`); return true; } /** * Destroy a session */ destroySession(sessionId: string): boolean { const existed = this.sessionStore.has(sessionId); this.sessionStore.delete(sessionId); if (existed) { logger.debug(`Session destroyed: ${sessionId.substring(0, 12)}...`); } return existed; } /** * Get session statistics */ getSessionStats(): { total: number; active: number; expired: number } { const now = Date.now(); let active = 0; let expired = 0; for (const session of this.sessionStore.values()) { if (now > session.expiresAt) { expired++; } else { active++; } } return { total: this.sessionStore.size, active, expired }; } /** * Generate a cryptographically secure session ID * Uses crypto.randomBytes for secure random generation as required by MCP spec */ private generateSecureSessionId(): string { // Use cryptographically secure random generation const secureRandomBytes = randomBytes(32); const timestamp = Date.now().toString(36); const secureRandom = secureRandomBytes.toString('hex'); return `sess_${timestamp}_${secureRandom}`; } /** * Start the automatic cleanup process */ private startCleanupProcess(): void { this.cleanupTimer = setInterval(() => { this.cleanupExpiredSessions(); }, this.cleanupInterval); } /** * Clean up expired sessions */ private cleanupExpiredSessions(): void { const now = Date.now(); let cleanedCount = 0; for (const [sessionId, session] of this.sessionStore.entries()) { if (now > session.expiresAt) { this.sessionStore.delete(sessionId); cleanedCount++; } } if (cleanedCount > 0) { logger.debug(`Cleaned up ${cleanedCount} expired sessions`); } } /** * Shutdown the token security manager */ shutdown(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; } this.sessionStore.clear(); logger.info('Token Security Manager shutdown'); } } // Export singleton instance export const tokenSecurityManager = TokenSecurityManager.getInstance(); // Export utility functions export const redactSensitiveTokens = (input: any) => tokenSecurityManager.redactSensitiveTokens(input); export const validateNoTokenPassthrough = (input: any, context: string) => tokenSecurityManager.validateNoTokenPassthrough(input, context); // Session management exports export const sessionManager = { create: (metadata?: SessionMetadata) => tokenSecurityManager.createSession(metadata), validate: (sessionId: string) => tokenSecurityManager.validateSession(sessionId), getMetadata: (sessionId: string) => tokenSecurityManager.getSessionMetadata(sessionId), updateMetadata: (sessionId: string, metadata: SessionMetadata) => tokenSecurityManager.updateSessionMetadata(sessionId, metadata), destroy: (sessionId: string) => tokenSecurityManager.destroySession(sessionId), getStats: () => tokenSecurityManager.getSessionStats() };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/PixdataOrg/coderide-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server