UNPKG

voyage-and-consumption-mcp-server

Version:

Voyage and consumption management server handling vessel voyages, fuel consumption, performance monitoring, and operational data with ERP access for data extraction

309 lines 11.2 kB
import { logger } from '../utils/logger.js'; export class SanitizationMiddleware { /** * Sanitizes input parameters to prevent MongoDB injection attacks * @param input - The input to sanitize * @param options - Sanitization options * @returns Sanitized input */ static sanitizeInput(input, options = {}) { const { allowRegExp = false, maxStringLength = 10000, allowedOperators = [], preventJavaScriptInjection = true } = options; if (input === null || input === undefined) { return input; } // Handle different types if (typeof input === 'string') { return this.sanitizeString(input, maxStringLength, preventJavaScriptInjection); } if (typeof input === 'number' || typeof input === 'boolean') { return input; } if (Array.isArray(input)) { return input.map(item => this.sanitizeInput(item, options)); } if (typeof input === 'object') { return this.sanitizeObject(input, options); } return input; } /** * Sanitizes string input */ static sanitizeString(input, maxLength, preventJavaScript) { // Length check if (input.length > maxLength) { logger.warn(`String length ${input.length} exceeds maximum ${maxLength}, truncating`); input = input.substring(0, maxLength); } // Remove null bytes input = input.replace(/\0/g, ''); // Prevent JavaScript injection if required if (preventJavaScript) { // Remove dangerous patterns for (const pattern of this.DANGEROUS_PATTERNS) { input = input.replace(pattern, ''); } } return input; } /** * Sanitizes object input */ static sanitizeObject(input, options) { const sanitized = {}; for (const [key, value] of Object.entries(input)) { // Check for dangerous operators if (this.isDangerousOperator(key, options.allowedOperators)) { logger.warn(`Dangerous operator detected and removed: ${key}`); continue; } // Sanitize key const sanitizedKey = this.sanitizeObjectKey(key); if (sanitizedKey !== key) { logger.warn(`Object key sanitized: ${key} -> ${sanitizedKey}`); } // Recursively sanitize value sanitized[sanitizedKey] = this.sanitizeInput(value, options); } return sanitized; } /** * Sanitizes object keys */ static sanitizeObjectKey(key) { // Remove dangerous characters return key.replace(/[^\w$.-]/g, ''); } /** * Checks if an operator is dangerous */ static isDangerousOperator(operator, allowedOperators = []) { // If it's in the allowed list, it's safe if (allowedOperators.includes(operator)) { return false; } // Check if it's a dangerous operator return this.DANGEROUS_OPERATORS.includes(operator); } /** * Sanitizes MongoDB query parameters */ static sanitizeMongoQuery(query, options = {}) { const sanitizationOptions = { allowRegExp: false, maxStringLength: 1000, allowedOperators: ['$eq', '$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin', '$and', '$or', '$exists'], preventJavaScriptInjection: true, ...options }; return this.sanitizeInput(query, sanitizationOptions); } /** * Sanitizes RegExp patterns to prevent injection */ static sanitizeRegExpPattern(pattern) { // Remove dangerous regex patterns const dangerousRegexPatterns = [ /\(\?\(/g, // Conditional expressions /\(\?\=/g, // Positive lookahead /\(\?\!/g, // Negative lookahead /\(\?\</g, // Positive lookbehind /\(\?\!/g, // Negative lookbehind /\(\?\>/g, // Atomic groups /\(\?\#/g, // Comments /\(\?\:/g, // Non-capturing groups (some contexts) ]; let sanitized = pattern; for (const dangerousPattern of dangerousRegexPatterns) { sanitized = sanitized.replace(dangerousPattern, ''); } // Escape special regex characters to prevent injection sanitized = sanitized.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return sanitized; } /** * Creates a safe regex pattern for MongoDB queries */ static createSafeRegex(pattern, flags = 'i') { const sanitizedPattern = this.sanitizeRegExpPattern(pattern); return new RegExp(sanitizedPattern, flags); } /** * Sanitizes Typesense query parameters */ static sanitizeTypesenseQuery(query) { const sanitized = {}; for (const [key, value] of Object.entries(query)) { // Sanitize key const sanitizedKey = key.replace(/[^\w.-]/g, ''); // Sanitize value based on type if (typeof value === 'string') { // For Typesense queries, we need to be more permissive but still safe sanitized[sanitizedKey] = this.sanitizeTypesenseString(value); } else if (Array.isArray(value)) { sanitized[sanitizedKey] = value.map(item => typeof item === 'string' ? this.sanitizeTypesenseString(item) : item); } else { sanitized[sanitizedKey] = value; } } return sanitized; } /** * Sanitizes Typesense search strings */ static sanitizeTypesenseString(input) { // Remove dangerous patterns but allow search operators let sanitized = input; // Remove JavaScript injection patterns for (const pattern of this.DANGEROUS_PATTERNS) { sanitized = sanitized.replace(pattern, ''); } // Remove null bytes sanitized = sanitized.replace(/\0/g, ''); // Limit length if (sanitized.length > 1000) { sanitized = sanitized.substring(0, 1000); } return sanitized; } /** * Sanitizes URL parameters */ static sanitizeUrlParams(params) { const sanitized = {}; for (const [key, value] of Object.entries(params)) { // Sanitize key const sanitizedKey = encodeURIComponent(key); // Sanitize value if (typeof value === 'string') { sanitized[sanitizedKey] = encodeURIComponent(value); } else if (typeof value === 'number' || typeof value === 'boolean') { sanitized[sanitizedKey] = value; } else if (Array.isArray(value)) { sanitized[sanitizedKey] = value.map(item => typeof item === 'string' ? encodeURIComponent(item) : item); } else { sanitized[sanitizedKey] = String(value); } } return sanitized; } /** * Validates and sanitizes date strings */ static sanitizeDateString(dateString) { // Remove any non-date characters const sanitized = dateString.replace(/[^\d-T:Z.]/g, ''); // Validate format if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/.test(sanitized)) { throw new Error(`Invalid date format: ${dateString}`); } return sanitized; } /** * Sanitizes IMO numbers */ static sanitizeIMO(imo) { const imoString = String(imo); // Remove non-numeric characters const sanitized = imoString.replace(/[^\d]/g, ''); // Validate length (IMO numbers are 7 digits) if (sanitized.length !== 7) { throw new Error(`Invalid IMO format: ${imo}`); } return sanitized; } /** * Sanitizes coordinate values */ static sanitizeCoordinate(coordinate, type) { if (typeof coordinate !== 'number' || isNaN(coordinate) || !isFinite(coordinate)) { throw new Error(`Invalid coordinate value: ${coordinate}`); } const min = type === 'latitude' ? -90 : -180; const max = type === 'latitude' ? 90 : 180; if (coordinate < min || coordinate > max) { throw new Error(`${type} ${coordinate} is outside valid range [${min}, ${max}]`); } return coordinate; } /** * Sanitizes HTML content to prevent XSS */ static sanitizeHtml(html) { // Simple HTML sanitization - remove script tags and event handlers return html .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') .replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '') .replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '') .replace(/<embed\b[^<]*(?:(?!<\/embed>)<[^<]*)*<\/embed>/gi, '') .replace(/on\w+\s*=\s*"[^"]*"/gi, '') .replace(/on\w+\s*=\s*'[^']*'/gi, '') .replace(/javascript:/gi, ''); } /** * Validates that a query object is safe */ static validateQuerySafety(query) { const queryString = JSON.stringify(query); // Check for dangerous patterns for (const pattern of this.DANGEROUS_PATTERNS) { if (pattern.test(queryString)) { logger.warn(`Dangerous pattern detected in query: ${pattern}`); return false; } } // Check for dangerous operators for (const operator of this.DANGEROUS_OPERATORS) { if (queryString.includes(operator)) { logger.warn(`Dangerous operator detected in query: ${operator}`); return false; } } return true; } /** * Logs security events */ static logSecurityEvent(event, details) { logger.warn(`Security event: ${event}`, { event, details, timestamp: new Date().toISOString() }); } } SanitizationMiddleware.MONGODB_OPERATORS = [ '$eq', '$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin', '$and', '$not', '$nor', '$or', '$exists', '$type', '$expr', '$jsonSchema', '$mod', '$regex', '$text', '$where', '$geoIntersects', '$geoWithin', '$near', '$nearSphere', '$all', '$elemMatch', '$size', '$bitsAllClear', '$bitsAllSet', '$bitsAnyClear', '$bitsAnySet' ]; SanitizationMiddleware.DANGEROUS_OPERATORS = [ '$where', '$expr', '$jsonSchema', '$function', '$accumulator', '$regex' ]; SanitizationMiddleware.DANGEROUS_PATTERNS = [ /\$where/gi, /\$expr/gi, /\$jsonSchema/gi, /\$function/gi, /\$accumulator/gi, /function\s*\(/gi, /eval\s*\(/gi, /setTimeout\s*\(/gi, /setInterval\s*\(/gi, /new\s+Function/gi, /javascript:/gi, /<script/gi, /on\w+\s*=/gi ]; // Export singleton for convenience export const sanitizationMiddleware = SanitizationMiddleware; //# sourceMappingURL=sanitization-middleware.js.map