UNPKG

survey-mcp-server

Version:

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

319 lines 11.1 kB
import { logger } from '../utils/logger.js'; export class SanitizationMiddleware { constructor() { } static getInstance() { if (!SanitizationMiddleware.instance) { SanitizationMiddleware.instance = new SanitizationMiddleware(); } return SanitizationMiddleware.instance; } sanitizeInput(input, options = {}) { const defaultOptions = { allowHtml: false, stripScripts: true, maxStringLength: 10000, allowedTags: [], allowedAttributes: [] }; const sanitizationOptions = { ...defaultOptions, ...options }; try { return this.sanitizeValue(input, sanitizationOptions); } catch (error) { logger.error('Sanitization error:', error); throw new Error('Input sanitization failed'); } } sanitizeValue(value, options) { if (value === null || value === undefined) { return value; } switch (typeof value) { case 'string': return this.sanitizeString(value, options); case 'number': return this.sanitizeNumber(value); case 'boolean': return value; case 'object': if (Array.isArray(value)) { return this.sanitizeArray(value, options); } else { return this.sanitizeObject(value, options); } default: return value; } } sanitizeString(str, options) { if (typeof str !== 'string') { return str; } let sanitized = str; // Trim whitespace sanitized = sanitized.trim(); // Limit string length if (options.maxStringLength && sanitized.length > options.maxStringLength) { sanitized = sanitized.substring(0, options.maxStringLength); } // Remove or escape dangerous characters if (!options.allowHtml) { sanitized = this.escapeHtml(sanitized); } else { sanitized = this.sanitizeHtml(sanitized, options); } // Remove script tags and javascript: URLs if (options.stripScripts) { sanitized = this.removeScripts(sanitized); } // Remove null bytes and other control characters sanitized = this.removeControlCharacters(sanitized); // Prevent path traversal sanitized = this.preventPathTraversal(sanitized); return sanitized; } sanitizeNumber(num) { if (typeof num !== 'number') { return num; } // Check for NaN and Infinity if (isNaN(num) || !isFinite(num)) { return 0; } // Limit to safe integer range if (num > Number.MAX_SAFE_INTEGER) { return Number.MAX_SAFE_INTEGER; } if (num < Number.MIN_SAFE_INTEGER) { return Number.MIN_SAFE_INTEGER; } return num; } sanitizeArray(arr, options) { if (!Array.isArray(arr)) { return arr; } // Limit array size const maxArraySize = 1000; if (arr.length > maxArraySize) { logger.warn(`Array size ${arr.length} exceeds maximum ${maxArraySize}, truncating`); arr = arr.slice(0, maxArraySize); } return arr.map(item => this.sanitizeValue(item, options)); } sanitizeObject(obj, options) { if (typeof obj !== 'object' || obj === null) { return obj; } // Prevent prototype pollution if (this.isPrototypePollution(obj)) { logger.warn('Prototype pollution attempt detected and blocked'); return {}; } const sanitized = {}; const maxObjectKeys = 100; let keyCount = 0; for (const [key, value] of Object.entries(obj)) { // Limit object size if (keyCount >= maxObjectKeys) { logger.warn(`Object key count ${Object.keys(obj).length} exceeds maximum ${maxObjectKeys}, truncating`); break; } // Sanitize key const sanitizedKey = this.sanitizeString(key, { ...options, allowHtml: false }); // Skip dangerous keys if (this.isDangerousKey(sanitizedKey)) { continue; } // Sanitize value sanitized[sanitizedKey] = this.sanitizeValue(value, options); keyCount++; } return sanitized; } escapeHtml(str) { const htmlEscapes = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '/': '&#x2F;' }; return str.replace(/[&<>"'/]/g, (match) => htmlEscapes[match]); } sanitizeHtml(str, options) { const allowedTags = options.allowedTags || []; const allowedAttributes = options.allowedAttributes || []; // Remove script tags and event handlers str = str.replace(/<script[\s\S]*?<\/script>/gi, ''); str = str.replace(/on\w+="[^"]*"/gi, ''); str = str.replace(/on\w+='[^']*'/gi, ''); str = str.replace(/javascript:/gi, ''); // If no allowed tags, escape all HTML if (allowedTags.length === 0) { return this.escapeHtml(str); } // Simple tag whitelist implementation const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/gi; return str.replace(tagRegex, (match, tagName) => { if (allowedTags.includes(tagName.toLowerCase())) { // Further sanitize attributes if needed return this.sanitizeAttributes(match, allowedAttributes); } return ''; }); } sanitizeAttributes(tagStr, allowedAttributes) { if (allowedAttributes.length === 0) { // Remove all attributes return tagStr.replace(/\s+[a-zA-Z][a-zA-Z0-9]*="[^"]*"/gi, ''); } // Simple attribute whitelist implementation const attrRegex = /\s+([a-zA-Z][a-zA-Z0-9]*)="[^"]*"/gi; return tagStr.replace(attrRegex, (match, attrName) => { if (allowedAttributes.includes(attrName.toLowerCase())) { return match; } return ''; }); } removeScripts(str) { // Remove script tags str = str.replace(/<script[\s\S]*?<\/script>/gi, ''); // Remove javascript: URLs str = str.replace(/javascript:/gi, ''); // Remove event handlers str = str.replace(/on\w+\s*=\s*"[^"]*"/gi, ''); str = str.replace(/on\w+\s*=\s*'[^']*'/gi, ''); // Remove data: URLs with javascript str = str.replace(/data:\s*[^;]*;base64\s*,\s*[^"']*/gi, ''); return str; } removeControlCharacters(str) { // Remove null bytes and other control characters except newlines and tabs return str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); } preventPathTraversal(str) { // Remove path traversal attempts str = str.replace(/\.\.\//g, ''); str = str.replace(/\.\.\\/g, ''); str = str.replace(/\.\./g, ''); return str; } isPrototypePollution(obj) { const dangerousKeys = ['__proto__', 'constructor', 'prototype']; for (const key of dangerousKeys) { if (key in obj) { return true; } } return false; } isDangerousKey(key) { const dangerousKeys = [ '__proto__', 'constructor', 'prototype', 'eval', 'function', 'require', 'import', 'export', 'process', 'global', 'window', 'document' ]; return dangerousKeys.includes(key.toLowerCase()); } sanitizeMongoQuery(query) { // Specific sanitization for MongoDB queries if (typeof query !== 'object' || query === null) { return query; } const sanitized = {}; for (const [key, value] of Object.entries(query)) { // Remove dangerous MongoDB operators if (key.startsWith('$') && this.isDangerousMongoOperator(key)) { continue; } // Sanitize regex patterns if (key === '$regex' || (typeof value === 'object' && value !== null && '$regex' in value)) { sanitized[key] = this.sanitizeRegex(value); } else { sanitized[key] = this.sanitizeValue(value, { allowHtml: false }); } } return sanitized; } isDangerousMongoOperator(operator) { const dangerousOperators = [ '$where', '$eval', '$function', '$accumulator', '$facet' ]; return dangerousOperators.includes(operator); } sanitizeRegex(regexValue) { if (typeof regexValue === 'string') { // Escape dangerous regex characters return regexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } if (typeof regexValue === 'object' && regexValue !== null && '$regex' in regexValue) { return { ...regexValue, $regex: this.sanitizeRegex(regexValue.$regex) }; } return regexValue; } sanitizeResponse(response) { // Sanitize response data to prevent information leakage if (typeof response !== 'object' || response === null) { return response; } const sanitized = {}; for (const [key, value] of Object.entries(response)) { // Remove sensitive fields if (this.isSensitiveField(key)) { continue; } // Recursively sanitize nested objects if (typeof value === 'object' && value !== null) { sanitized[key] = this.sanitizeResponse(value); } else if (typeof value === 'string') { // Sanitize string values sanitized[key] = this.sanitizeString(value, { allowHtml: false }); } else { sanitized[key] = value; } } return sanitized; } isSensitiveField(fieldName) { const sensitiveFields = [ 'password', 'secret', 'token', 'key', 'credential', 'auth', 'session', 'cookie', 'private', 'internal' ]; const fieldLower = fieldName.toLowerCase(); return sensitiveFields.some(sensitive => fieldLower.includes(sensitive)); } } export const sanitizationMiddleware = SanitizationMiddleware.getInstance(); //# sourceMappingURL=sanitization.js.map