Skip to main content
Glama
security.ts7.55 kB
// Security utilities for Ultimate Elementor MCP import { logger } from './logger.js'; import { MCPError, ErrorCategory } from './error-handler.js'; /** * Input Validation and Sanitization */ export class InputValidator { /** * Validate and sanitize post ID */ static validatePostId(postId: any): number { const id = parseInt(postId); if (isNaN(id) || id <= 0) { throw new MCPError( 'Invalid post ID. Must be a positive integer', ErrorCategory.VALIDATION, 'INVALID_POST_ID' ); } return id; } /** * Validate and sanitize URL */ static validateUrl(url: string): string { try { const parsed = new URL(url); if (!['http:', 'https:'].includes(parsed.protocol)) { throw new Error('Invalid protocol'); } return url; } catch (error) { throw new MCPError( 'Invalid URL format. Must be http:// or https://', ErrorCategory.VALIDATION, 'INVALID_URL', { url } ); } } /** * Sanitize file path (prevent directory traversal) */ static sanitizeFilePath(filePath: string): string { // Remove any directory traversal attempts const sanitized = filePath.replace(/\.\./g, '').replace(/\/\//g, '/'); // Check for suspicious patterns if (filePath.includes('..') || filePath.includes('~')) { logger.warn('Suspicious file path detected', { original: filePath, sanitized }); } return sanitized; } /** * Validate element ID format */ static validateElementId(elementId: string): string { if (!elementId || typeof elementId !== 'string' || elementId.length === 0) { throw new MCPError( 'Invalid element ID', ErrorCategory.VALIDATION, 'INVALID_ELEMENT_ID' ); } return elementId; } /** * Sanitize HTML content (basic sanitization) */ static sanitizeHtml(html: string): string { // Remove potentially dangerous tags const dangerous = ['script', 'iframe', 'object', 'embed', 'applet']; let sanitized = html; dangerous.forEach(tag => { const regex = new RegExp(`<${tag}[^>]*>.*?</${tag}>`, 'gi'); sanitized = sanitized.replace(regex, ''); }); return sanitized; } /** * Validate JSON string */ static validateJson(jsonString: string, fieldName: string = 'data'): any { try { return JSON.parse(jsonString); } catch (error) { throw new MCPError( `Invalid JSON in ${fieldName}`, ErrorCategory.VALIDATION, 'INVALID_JSON', { error } ); } } /** * Validate string length */ static validateStringLength( str: string, maxLength: number, fieldName: string = 'field' ): string { if (str.length > maxLength) { throw new MCPError( `${fieldName} exceeds maximum length of ${maxLength} characters`, ErrorCategory.VALIDATION, 'STRING_TOO_LONG', { length: str.length, maxLength } ); } return str; } } /** * Rate Limiting */ export class RateLimiter { private requests: Map<string, number[]> = new Map(); private readonly windowMs: number = 60000; // 1 minute private readonly maxRequests: number = 100; // per minute constructor(maxRequests: number = 100, windowMs: number = 60000) { this.maxRequests = maxRequests; this.windowMs = windowMs; } /** * Check if request is allowed */ isAllowed(identifier: string): boolean { const now = Date.now(); const requests = this.requests.get(identifier) || []; // Remove old requests outside the time window const validRequests = requests.filter(time => now - time < this.windowMs); if (validRequests.length >= this.maxRequests) { logger.warn('Rate limit exceeded', { identifier, count: validRequests.length }); return false; } // Add current request validRequests.push(now); this.requests.set(identifier, validRequests); return true; } /** * Get remaining requests in window */ getRemaining(identifier: string): number { const now = Date.now(); const requests = this.requests.get(identifier) || []; const validRequests = requests.filter(time => now - time < this.windowMs); return Math.max(0, this.maxRequests - validRequests.length); } /** * Reset rate limit for identifier */ reset(identifier: string): void { this.requests.delete(identifier); } /** * Clear all rate limit data */ clearAll(): void { this.requests.clear(); } } // Global rate limiter instance export const rateLimiter = new RateLimiter(); /** * Authentication Security */ export class AuthenticationSecurity { /** * Validate Application Password format */ static validateApplicationPassword(password: string): boolean { // WordPress Application Passwords are typically 24 characters with spaces // Format: xxxx xxxx xxxx xxxx xxxx xxxx if (!password || password.trim().length === 0) { throw new MCPError( 'Application password cannot be empty', ErrorCategory.AUTHENTICATION, 'EMPTY_PASSWORD' ); } // Check if it looks like a regular password (warn user) if (password.length < 20 && !password.includes(' ')) { logger.warn('Password may not be an Application Password. WordPress Application Passwords are longer and contain spaces.'); } return true; } /** * Validate username */ static validateUsername(username: string): boolean { if (!username || username.trim().length === 0) { throw new MCPError( 'Username cannot be empty', ErrorCategory.AUTHENTICATION, 'EMPTY_USERNAME' ); } if (username.length > 60) { throw new MCPError( 'Username is too long', ErrorCategory.AUTHENTICATION, 'USERNAME_TOO_LONG' ); } return true; } } /** * Secure File Operations */ export class SecureFileOperations { private static readonly ALLOWED_EXTENSIONS = ['.json', '.txt', '.md']; private static readonly MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB /** * Validate file extension */ static validateFileExtension(filePath: string): boolean { const ext = filePath.toLowerCase().substring(filePath.lastIndexOf('.')); if (!this.ALLOWED_EXTENSIONS.includes(ext)) { throw new MCPError( `File extension ${ext} not allowed. Allowed: ${this.ALLOWED_EXTENSIONS.join(', ')}`, ErrorCategory.FILE_OPERATION, 'INVALID_FILE_EXTENSION', { ext, allowed: this.ALLOWED_EXTENSIONS } ); } return true; } /** * Validate file size */ static validateFileSize(size: number): boolean { if (size > this.MAX_FILE_SIZE) { throw new MCPError( `File size exceeds maximum allowed size of ${this.MAX_FILE_SIZE / 1024 / 1024}MB`, ErrorCategory.FILE_OPERATION, 'FILE_TOO_LARGE', { size, maxSize: this.MAX_FILE_SIZE } ); } return true; } /** * Check if path is within allowed directory */ static isPathSafe(filePath: string, baseDir: string): boolean { const path = require('path'); const resolvedPath = path.resolve(filePath); const resolvedBase = path.resolve(baseDir); if (!resolvedPath.startsWith(resolvedBase)) { throw new MCPError( 'File path is outside allowed directory', ErrorCategory.FILE_OPERATION, 'UNSAFE_PATH', { filePath, baseDir } ); } return true; } }

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/mbrown1837/Ultimate-Elementor-MCP'

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