UNPKG

survey-mcp-server

Version:

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

308 lines 10.7 kB
import { logger } from '../utils/logger.js'; export class SecurityValidator { constructor() { } static getInstance() { if (!SecurityValidator.instance) { SecurityValidator.instance = new SecurityValidator(); } return SecurityValidator.instance; } validateInput(input, options = {}) { const defaultOptions = { maxDepth: 10, maxKeys: 100, allowedTypes: ['string', 'number', 'boolean', 'object', 'array'], blacklistedPatterns: [ /eval\s*\(/i, /function\s*\(/i, /require\s*\(/i, /import\s*\(/i, /process\s*\./i, /global\s*\./i, /window\s*\./i, /document\s*\./i, /script\s*>/i, /javascript\s*:/i, /data\s*:\s*text\/html/i, /vbscript\s*:/i ] }; const validationOptions = { ...defaultOptions, ...options }; const issues = []; try { this.validateValue(input, validationOptions, issues, 0, 'root'); if (issues.length > 0) { logger.warn('Security validation issues found:', issues); } return { isValid: issues.length === 0, issues }; } catch (error) { logger.error('Security validation error:', error); return { isValid: false, issues: [`Validation error: ${error.message}`] }; } } validateValue(value, options, issues, depth, path) { // Check depth limit if (depth > (options.maxDepth || 10)) { issues.push(`Maximum depth exceeded at ${path}`); return; } // Check type const valueType = Array.isArray(value) ? 'array' : typeof value; if (options.allowedTypes && !options.allowedTypes.includes(valueType)) { issues.push(`Disallowed type '${valueType}' at ${path}`); return; } // String-specific validations if (typeof value === 'string') { this.validateString(value, options, issues, path); } // Object-specific validations if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { this.validateArray(value, options, issues, depth, path); } else { this.validateObject(value, options, issues, depth, path); } } } validateString(value, options, issues, path) { // Check for malicious patterns if (options.blacklistedPatterns) { for (const pattern of options.blacklistedPatterns) { if (pattern.test(value)) { issues.push(`Suspicious pattern detected in string at ${path}: ${pattern.source}`); } } } // Check for common injection attempts this.checkForInjectionAttempts(value, issues, path); // Check for suspicious keywords this.checkForSuspiciousKeywords(value, issues, path); } validateArray(value, options, issues, depth, path) { // Check array size if (value.length > 1000) { issues.push(`Array too large (${value.length} items) at ${path}`); return; } // Validate each item for (let i = 0; i < value.length; i++) { this.validateValue(value[i], options, issues, depth + 1, `${path}[${i}]`); } } validateObject(value, options, issues, depth, path) { const keys = Object.keys(value); // Check object size if (keys.length > (options.maxKeys || 100)) { issues.push(`Object too large (${keys.length} keys) at ${path}`); return; } // Check for prototype pollution if (this.checkForPrototypePollution(value, issues, path)) { return; // Stop processing if prototype pollution detected } // Validate each property for (const key of keys) { // Validate key name this.validatePropertyKey(key, issues, path); // Validate value this.validateValue(value[key], options, issues, depth + 1, `${path}.${key}`); } } checkForInjectionAttempts(value, issues, path) { // SQL injection patterns const sqlPatterns = [ /(\s|^)(union|select|insert|update|delete|drop|create|alter)\s/i, /(\s|^)(or|and)\s+\d+\s*=\s*\d+/i, /('|"|`)\s*(or|and|union)\s*\1/i, /;\s*(drop|delete|update|insert)/i ]; // NoSQL injection patterns const nosqlPatterns = [ /\$where/i, /\$eval/i, /\$function/i, /\$regex.*\$options/i, /\{\s*\$ne\s*:\s*null\s*\}/i ]; // Command injection patterns const commandPatterns = [ /;.*(?:rm|del|format|shutdown|reboot)/i, /\|.*(?:cat|type|more|less)/i, /&&.*(?:rm|del|format)/i, /`.*`/, /\$\(.*\)/ ]; const allPatterns = [...sqlPatterns, ...nosqlPatterns, ...commandPatterns]; for (const pattern of allPatterns) { if (pattern.test(value)) { issues.push(`Possible injection attempt detected at ${path}: ${pattern.source}`); } } } checkForSuspiciousKeywords(value, issues, path) { const suspiciousKeywords = [ 'eval', 'function', 'constructor', 'prototype', '__proto__', 'require', 'import', 'process', 'global', 'window', 'document', 'location', 'navigator', 'cookie', 'localStorage', 'sessionStorage', 'indexedDB', 'webSQL', 'chrome', 'webkitStorageInfo' ]; const valueLower = value.toLowerCase(); for (const keyword of suspiciousKeywords) { if (valueLower.includes(keyword)) { issues.push(`Suspicious keyword '${keyword}' found at ${path}`); } } } validatePropertyKey(key, issues, path) { // Check for dangerous property names const dangerousKeys = [ '__proto__', 'constructor', 'prototype', 'eval', 'function', 'require', 'import', 'process', 'global' ]; if (dangerousKeys.includes(key)) { issues.push(`Dangerous property key '${key}' at ${path}`); } // Check for MongoDB operator abuse if (key.startsWith('$') && !this.isAllowedMongoOperator(key)) { issues.push(`Potentially dangerous MongoDB operator '${key}' at ${path}`); } } checkForPrototypePollution(obj, issues, path) { const pollutionKeys = ['__proto__', 'constructor', 'prototype']; for (const key of pollutionKeys) { if (key in obj) { issues.push(`Prototype pollution attempt detected at ${path}.${key}`); return true; } } return false; } isAllowedMongoOperator(operator) { const allowedOperators = [ // Comparison '$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', // Logical '$and', '$or', '$not', '$nor', // Element '$exists', '$type', // Array '$all', '$elemMatch', '$size', // Text '$text', '$search', // Regex (with caution) '$regex', '$options' ]; return allowedOperators.includes(operator); } validateIMO(imo) { const issues = []; // Convert to string const imoStr = String(imo).trim(); // Check that it contains only digits if (!/^\d+$/.test(imoStr)) { issues.push('IMO must contain only digits'); return { isValid: false, issues }; } return { isValid: true, normalizedIMO: imoStr, issues: [] }; } validateEmail(email) { const issues = []; // Basic email regex (RFC 5322 compliant) const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; if (!emailRegex.test(email)) { issues.push('Invalid email format'); return { isValid: false, issues }; } // Check for suspicious patterns in email const suspiciousPatterns = [ /script/i, /javascript/i, /vbscript/i, /onload/i, /onerror/i ]; for (const pattern of suspiciousPatterns) { if (pattern.test(email)) { issues.push('Email contains suspicious content'); return { isValid: false, issues }; } } return { isValid: true, normalizedEmail: email.toLowerCase().trim(), issues: [] }; } validateURL(url) { const issues = []; try { const urlObj = new URL(url); // Check for allowed protocols const allowedProtocols = ['http:', 'https:']; if (!allowedProtocols.includes(urlObj.protocol)) { issues.push(`Protocol '${urlObj.protocol}' not allowed`); return { isValid: false, issues }; } // Check for suspicious patterns const suspiciousPatterns = [ /javascript:/i, /vbscript:/i, /data:/i, /file:/i ]; for (const pattern of suspiciousPatterns) { if (pattern.test(url)) { issues.push('URL contains suspicious protocol or content'); return { isValid: false, issues }; } } return { isValid: true, normalizedURL: urlObj.toString(), issues: [] }; } catch (error) { issues.push('Invalid URL format'); return { isValid: false, issues }; } } } export const securityValidator = SecurityValidator.getInstance(); //# sourceMappingURL=validation.js.map