UNPKG

web-vuln-scanner

Version:

Advanced, lightweight web vulnerability scanner with smart detection and easy-to-use interface

363 lines (309 loc) 9.96 kB
/** * Security and Input Validation Module * Production-ready security utilities for web vulnerability scanner */ const crypto = require('crypto'); const { URL } = require('url'); class SecurityValidator { constructor() { this.MAX_URL_LENGTH = 2048; this.MAX_HEADER_LENGTH = 8192; this.MAX_PAYLOAD_SIZE = 1024 * 1024; // 1MB this.ALLOWED_PROTOCOLS = ['http:', 'https:']; this.FORBIDDEN_HEADERS = ['authorization', 'cookie', 'x-forwarded-for']; this.RATE_LIMIT_WINDOW = 60000; // 1 minute this.MAX_REQUESTS_PER_WINDOW = 100; // Rate limiting store this.rateLimitStore = new Map(); // Cleanup rate limit store every 5 minutes setInterval(() => this.cleanupRateLimit(), 5 * 60 * 1000); } /** * Validate and sanitize URL input * @param {string} url - URL to validate * @returns {object} - Validation result */ validateUrl(url) { const result = { isValid: false, sanitized: null, errors: [] }; if (!url || typeof url !== 'string') { result.errors.push('URL must be a non-empty string'); return result; } // Check length if (url.length > this.MAX_URL_LENGTH) { result.errors.push(`URL exceeds maximum length of ${this.MAX_URL_LENGTH} characters`); return result; } // Check for dangerous characters const dangerousChars = /[\x00-\x1f\x7f-\x9f]/; if (dangerousChars.test(url)) { result.errors.push('URL contains dangerous control characters'); return result; } try { const parsedUrl = new URL(url); // Validate protocol if (!this.ALLOWED_PROTOCOLS.includes(parsedUrl.protocol)) { result.errors.push(`Protocol ${parsedUrl.protocol} is not allowed`); return result; } // Block local/private IPs in production if (this.isLocalOrPrivateIP(parsedUrl.hostname)) { result.errors.push('Local and private IP addresses are not allowed'); return result; } // Sanitize and normalize result.sanitized = parsedUrl.href; result.isValid = true; } catch (error) { result.errors.push(`Invalid URL format: ${error.message}`); } return result; } /** * Check if hostname is local or private IP * @param {string} hostname * @returns {boolean} */ isLocalOrPrivateIP(hostname) { // localhost patterns if (['localhost', '127.0.0.1', '::1'].includes(hostname)) { return true; } // Private IP ranges const privateRanges = [ /^10\./, /^172\.(1[6-9]|2[0-9]|3[0-1])\./, /^192\.168\./, /^169\.254\./, // Link-local /^fc00::/i, // IPv6 private /^fe80::/i // IPv6 link-local ]; return privateRanges.some(range => range.test(hostname)); } /** * Validate HTTP headers * @param {object} headers * @returns {object} */ validateHeaders(headers) { const result = { isValid: false, sanitized: {}, errors: [] }; if (!headers || typeof headers !== 'object') { result.isValid = true; result.sanitized = {}; return result; } for (const [key, value] of Object.entries(headers)) { // Validate header name if (typeof key !== 'string' || key.length === 0) { result.errors.push('Header names must be non-empty strings'); continue; } // Check for forbidden headers if (this.FORBIDDEN_HEADERS.includes(key.toLowerCase())) { result.errors.push(`Header ${key} is not allowed`); continue; } // Validate header value if (typeof value !== 'string') { result.errors.push(`Header ${key} value must be a string`); continue; } // Check length if (value.length > this.MAX_HEADER_LENGTH) { result.errors.push(`Header ${key} exceeds maximum length`); continue; } // Sanitize header value (remove dangerous characters) const sanitizedValue = value.replace(/[\x00-\x1f\x7f-\x9f]/g, ''); result.sanitized[key] = sanitizedValue; } result.isValid = result.errors.length === 0; return result; } /** * Validate scan configuration * @param {object} config * @returns {object} */ validateScanConfig(config) { const result = { isValid: false, sanitized: {}, errors: [] }; if (!config || typeof config !== 'object') { result.errors.push('Configuration must be an object'); return result; } // Validate timeout if (config.timeout !== undefined) { const timeout = parseInt(config.timeout); if (isNaN(timeout) || timeout < 1000 || timeout > 300000) { result.errors.push('Timeout must be between 1000ms and 300000ms'); } else { result.sanitized.timeout = timeout; } } // Validate depth if (config.depth !== undefined) { const depth = parseInt(config.depth); if (isNaN(depth) || depth < 0 || depth > 10) { result.errors.push('Depth must be between 0 and 10'); } else { result.sanitized.depth = depth; } } // Validate concurrency if (config.concurrency !== undefined) { const concurrency = parseInt(config.concurrency); if (isNaN(concurrency) || concurrency < 1 || concurrency > 50) { result.errors.push('Concurrency must be between 1 and 50'); } else { result.sanitized.concurrency = concurrency; } } // Validate modules if (config.scanModules) { if (!Array.isArray(config.scanModules)) { result.errors.push('Scan modules must be an array'); } else { const validModules = [ 'headers', 'xss', 'sql', 'ssl', 'ports', 'dirTraversal', 'csrf', 'csp', 'versionCheck', 'rce', 'idor', 'misconfiguredHeaders', 'waf' ]; const invalidModules = config.scanModules.filter(m => !validModules.includes(m)); if (invalidModules.length > 0) { result.errors.push(`Invalid modules: ${invalidModules.join(', ')}`); } else { result.sanitized.scanModules = config.scanModules; } } } // Copy other valid configurations ['disableCrawler', 'enableJS', 'userAgent', 'benchmark'].forEach(key => { if (config[key] !== undefined) { result.sanitized[key] = config[key]; } }); result.isValid = result.errors.length === 0; return result; } /** * Rate limiting check * @param {string} identifier - Client identifier * @returns {boolean} - Whether request is allowed */ checkRateLimit(identifier) { const now = Date.now(); const windowStart = now - this.RATE_LIMIT_WINDOW; if (!this.rateLimitStore.has(identifier)) { this.rateLimitStore.set(identifier, []); } const requests = this.rateLimitStore.get(identifier); // Remove old requests outside the window const recentRequests = requests.filter(timestamp => timestamp > windowStart); if (recentRequests.length >= this.MAX_REQUESTS_PER_WINDOW) { return false; // Rate limit exceeded } // Add current request recentRequests.push(now); this.rateLimitStore.set(identifier, recentRequests); return true; } /** * Clean up old rate limit entries */ cleanupRateLimit() { const now = Date.now(); const windowStart = now - this.RATE_LIMIT_WINDOW; for (const [identifier, requests] of this.rateLimitStore) { const recentRequests = requests.filter(timestamp => timestamp > windowStart); if (recentRequests.length === 0) { this.rateLimitStore.delete(identifier); } else { this.rateLimitStore.set(identifier, recentRequests); } } } /** * Sanitize payload for logging * @param {string} payload * @returns {string} */ sanitizeForLogging(payload) { if (!payload || typeof payload !== 'string') return ''; return payload .replace(/[\x00-\x1f\x7f-\x9f]/g, '') // Remove control characters .substring(0, 1000) // Limit length .replace(/[<>"'&]/g, char => ({ // HTML encode dangerous chars '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#x27;', '&': '&amp;' })[char]); } /** * Generate secure random identifier * @returns {string} */ generateSecureId() { return crypto.randomBytes(16).toString('hex'); } /** * Hash sensitive data for storage * @param {string} data * @returns {string} */ hashSensitiveData(data) { return crypto.createHash('sha256').update(data).digest('hex'); } /** * Validate file path for output * @param {string} filePath * @returns {object} */ validateOutputPath(filePath) { const result = { isValid: false, sanitized: null, errors: [] }; if (!filePath || typeof filePath !== 'string') { result.errors.push('File path must be a non-empty string'); return result; } // Check for path traversal if (filePath.includes('..') || filePath.includes('~')) { result.errors.push('Path traversal attempts are not allowed'); return result; } // Check for dangerous characters const dangerousChars = /[\x00-\x1f\x7f-\x9f<>:"|?*]/; if (dangerousChars.test(filePath)) { result.errors.push('File path contains dangerous characters'); return result; } // Validate file extension const allowedExtensions = ['.html', '.json', '.md', '.txt', '.xml']; const hasValidExtension = allowedExtensions.some(ext => filePath.toLowerCase().endsWith(ext)); if (!hasValidExtension) { result.errors.push(`File extension must be one of: ${allowedExtensions.join(', ')}`); return result; } result.sanitized = filePath; result.isValid = true; return result; } } module.exports = { SecurityValidator };