UNPKG

survey-mcp-server

Version:

Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management

298 lines 11.3 kB
import { logger } from '../utils/logger.js'; export class SecuritySanitizer { constructor() { } static getInstance() { if (!SecuritySanitizer.instance) { SecuritySanitizer.instance = new SecuritySanitizer(); } return SecuritySanitizer.instance; } sanitizeForMongoDB(input) { try { return this.deepSanitizeObject(input, this.mongoDBSanitizer.bind(this)); } catch (error) { logger.error('MongoDB sanitization error:', error); throw new Error('MongoDB sanitization failed'); } } sanitizeForTypesense(input) { try { return this.deepSanitizeObject(input, this.typesenseSanitizer.bind(this)); } catch (error) { logger.error('Typesense sanitization error:', error); throw new Error('Typesense sanitization failed'); } } sanitizeForExternalAPI(input) { try { return this.deepSanitizeObject(input, this.externalAPISanitizer.bind(this)); } catch (error) { logger.error('External API sanitization error:', error); throw new Error('External API sanitization failed'); } } sanitizeCredentials(credentials) { const sanitized = { ...credentials }; // Remove or mask sensitive fields const sensitiveFields = ['password', 'secret', 'token', 'key', 'apiKey', 'accessToken']; for (const field of sensitiveFields) { if (field in sanitized) { if (typeof sanitized[field] === 'string' && sanitized[field].length > 0) { // Keep first 2 and last 2 characters, mask the rest const value = sanitized[field]; if (value.length > 4) { sanitized[field] = value.substring(0, 2) + '*'.repeat(value.length - 4) + value.substring(value.length - 2); } else { sanitized[field] = '*'.repeat(value.length); } } else { sanitized[field] = '[REDACTED]'; } } } return sanitized; } deepSanitizeObject(obj, sanitizer) { if (obj === null || obj === undefined) { return obj; } if (typeof obj === 'string') { return sanitizer(obj); } if (typeof obj === 'number' || typeof obj === 'boolean') { return obj; } if (Array.isArray(obj)) { return obj.map(item => this.deepSanitizeObject(item, sanitizer)); } if (typeof obj === 'object') { const sanitized = {}; for (const [key, value] of Object.entries(obj)) { // Sanitize the key const sanitizedKey = sanitizer(key); // Skip dangerous keys if (this.isDangerousKey(sanitizedKey)) { continue; } // Recursively sanitize the value sanitized[sanitizedKey] = this.deepSanitizeObject(value, sanitizer); } return sanitized; } return obj; } mongoDBSanitizer(value) { if (typeof value !== 'string') { return value; } let sanitized = value; // Remove dangerous MongoDB operators const dangerousOperators = [ '$where', '$eval', '$function', '$accumulator', '$facet', '$expr' ]; for (const operator of dangerousOperators) { const regex = new RegExp(`\\${operator}`, 'gi'); sanitized = sanitized.replace(regex, ''); } // Escape special regex characters if it looks like a regex if (sanitized.includes('$regex')) { sanitized = this.escapeRegexSpecialChars(sanitized); } // Remove potential NoSQL injection patterns const injectionPatterns = [ /\{\s*\$ne\s*:\s*null\s*\}/gi, /\{\s*\$gt\s*:\s*""\s*\}/gi, /\{\s*\$regex\s*:\s*".*"\s*\}/gi, /\{\s*\$exists\s*:\s*true\s*\}/gi ]; for (const pattern of injectionPatterns) { sanitized = sanitized.replace(pattern, ''); } return sanitized; } typesenseSanitizer(value) { if (typeof value !== 'string') { return value; } let sanitized = value; // Remove or escape Typesense special characters const specialChars = ['*', '?', ':', '(', ')', '[', ']', '{', '}', '^', '~']; for (const char of specialChars) { const regex = new RegExp(`\\${char}`, 'g'); sanitized = sanitized.replace(regex, `\\${char}`); } // Remove potential script injections sanitized = this.removeScriptContent(sanitized); return sanitized; } externalAPISanitizer(value) { if (typeof value !== 'string') { return value; } let sanitized = value; // Remove script content sanitized = this.removeScriptContent(sanitized); // Remove event handlers sanitized = this.removeEventHandlers(sanitized); // Remove dangerous URLs sanitized = this.removeDangerousUrls(sanitized); // Limit string length if (sanitized.length > 10000) { sanitized = sanitized.substring(0, 10000); } return sanitized; } removeScriptContent(str) { // Remove script tags and their content str = str.replace(/<script[\s\S]*?<\/script>/gi, ''); // Remove javascript: URLs str = str.replace(/javascript\s*:/gi, ''); // Remove vbscript: URLs str = str.replace(/vbscript\s*:/gi, ''); // Remove data: URLs with executable content str = str.replace(/data\s*:\s*[^;]*;base64\s*,\s*[^"']*/gi, ''); str = str.replace(/data\s*:\s*text\/html[^"']*/gi, ''); return str; } removeEventHandlers(str) { // Remove HTML event handlers const eventHandlers = [ 'onload', 'onerror', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'onkeydown', 'onkeyup', 'onkeypress', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect', 'onabort', 'oncanplay', 'oncanplaythrough', 'ondurationchange', 'onemptied', 'onended', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled', 'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting' ]; for (const handler of eventHandlers) { const regex = new RegExp(`${handler}\\s*=\\s*["'][^"']*["']`, 'gi'); str = str.replace(regex, ''); } return str; } removeDangerousUrls(str) { // Remove dangerous protocol URLs const dangerousProtocols = [ 'javascript:', 'vbscript:', 'data:', 'file:', 'ftp:', 'jar:', 'chrome:', 'chrome-extension:', 'moz-extension:' ]; for (const protocol of dangerousProtocols) { const regex = new RegExp(protocol.replace(':', '\\s*:'), 'gi'); str = str.replace(regex, ''); } return str; } escapeRegexSpecialChars(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } isDangerousKey(key) { const dangerousKeys = [ '__proto__', 'constructor', 'prototype', 'eval', 'function', 'require', 'import', 'process', 'global', 'window', 'document' ]; return dangerousKeys.includes(key.toLowerCase()); } sanitizeLogData(data) { // Sanitize data for logging to prevent log injection const sanitized = this.deepSanitizeObject(data, (value) => { if (typeof value !== 'string') { return value; } // Remove newlines and carriage returns to prevent log injection let clean = value.replace(/[\r\n]/g, ' '); // Remove ANSI escape sequences clean = clean.replace(/\x1b\[[0-9;]*m/g, ''); // Remove other control characters clean = clean.replace(/[\x00-\x1F\x7F]/g, ''); // Limit length if (clean.length > 1000) { clean = clean.substring(0, 1000) + '...'; } return clean; }); // Remove sensitive fields return this.removeSensitiveFields(sanitized); } removeSensitiveFields(obj) { if (typeof obj !== 'object' || obj === null) { return obj; } if (Array.isArray(obj)) { return obj.map(item => this.removeSensitiveFields(item)); } const sanitized = {}; const sensitiveFields = [ 'password', 'secret', 'token', 'key', 'apikey', 'accesstoken', 'credential', 'auth', 'session', 'cookie', 'private' ]; for (const [key, value] of Object.entries(obj)) { const keyLower = key.toLowerCase(); const isSensitive = sensitiveFields.some(field => keyLower.includes(field)); if (isSensitive) { sanitized[key] = '[REDACTED]'; } else if (typeof value === 'object') { sanitized[key] = this.removeSensitiveFields(value); } else { sanitized[key] = value; } } return sanitized; } sanitizeError(error) { // Sanitize error objects to prevent sensitive information leakage const sanitized = { message: error.message || 'An error occurred', name: error.name || 'Error', timestamp: new Date().toISOString() }; // Include safe properties if (error.code && typeof error.code === 'string') { sanitized.code = error.code; } if (error.statusCode && typeof error.statusCode === 'number') { sanitized.statusCode = error.statusCode; } // Sanitize the error message if (typeof sanitized.message === 'string') { // Remove file paths sanitized.message = sanitized.message.replace(/\/[^\s]+/g, '[PATH]'); // Remove potential credentials sanitized.message = sanitized.message.replace(/password[=:]\s*[^\s]+/gi, 'password=[REDACTED]'); sanitized.message = sanitized.message.replace(/token[=:]\s*[^\s]+/gi, 'token=[REDACTED]'); sanitized.message = sanitized.message.replace(/key[=:]\s*[^\s]+/gi, 'key=[REDACTED]'); // Remove MongoDB connection strings sanitized.message = sanitized.message.replace(/mongodb:\/\/[^@]*@[^\/]+/gi, 'mongodb://[REDACTED]@[HOST]'); } return sanitized; } } export const securitySanitizer = SecuritySanitizer.getInstance(); //# sourceMappingURL=sanitization.js.map