// 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;
}
}