/** * Comprehensive Input Validation Utility * Implements whitelist-based validation for all user inputs */ const validator = require('validator'); /** * Validation Rules Configuration */ const VALIDATION_RULES = { // User-related username: { minLength: 3, maxLength: 50, pattern: /^[a-zA-Z0-9_-]+$/, sanitize: true }, email: { maxLength: 255, sanitize: true }, password: { minLength: 8, maxLength: 128 }, // Content-related playlistName: { minLength: 1, maxLength: 200, pattern: /^[a-zA-Z0-9\s\-_.,()!]+$/, sanitize: true }, channelName: { minLength: 1, maxLength: 200, sanitize: true }, url: { maxLength: 2048, protocols: ['http', 'https', 'rtmp', 'rtsp', 'udp', 'rtp'] }, // Generic text fields description: { maxLength: 1000, sanitize: true }, // File names filename: { maxLength: 255, pattern: /^[a-zA-Z0-9\s\-_.,()]+$/, sanitize: true }, // Settings keys settingKey: { maxLength: 100, pattern: /^[a-zA-Z0-9_.-]+$/ } }; /** * Sanitize string input to prevent XSS */ function sanitizeString(str) { if (typeof str !== 'string') return str; // Remove HTML tags str = str.replace(/<[^>]*>/g, ''); // Remove script-related content str = str.replace(/javascript:/gi, ''); str = str.replace(/on\w+\s*=/gi, ''); // Escape special characters return validator.escape(str); } // Export sanitizeString module.exports.sanitizeString = sanitizeString; /** * Validate username */ function validateUsername(username) { const errors = []; const rules = VALIDATION_RULES.username; if (!username || typeof username !== 'string') { errors.push('Username is required'); return { valid: false, errors, sanitized: null }; } const trimmed = username.trim(); if (trimmed.length < rules.minLength) { errors.push(`Username must be at least ${rules.minLength} characters`); } if (trimmed.length > rules.maxLength) { errors.push(`Username must not exceed ${rules.maxLength} characters`); } if (!rules.pattern.test(trimmed)) { errors.push('Username can only contain letters, numbers, hyphens, and underscores'); } const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed; return { valid: errors.length === 0, errors, sanitized }; } /** * Validate email */ function validateEmail(email) { const errors = []; const rules = VALIDATION_RULES.email; if (!email || typeof email !== 'string') { errors.push('Email is required'); return { valid: false, errors, sanitized: null }; } const trimmed = email.trim().toLowerCase(); if (!validator.isEmail(trimmed)) { errors.push('Invalid email format'); } if (trimmed.length > rules.maxLength) { errors.push(`Email must not exceed ${rules.maxLength} characters`); } const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed; return { valid: errors.length === 0, errors, sanitized }; } /** * Validate URL */ function validateUrl(url, allowLocalhost = false) { const errors = []; const rules = VALIDATION_RULES.url; if (!url || typeof url !== 'string') { errors.push('URL is required'); return { valid: false, errors, sanitized: null }; } const trimmed = url.trim(); if (trimmed.length > rules.maxLength) { errors.push(`URL must not exceed ${rules.maxLength} characters`); } // Check if URL is valid and uses allowed protocols const options = { protocols: rules.protocols, require_protocol: true, allow_underscores: true }; if (!allowLocalhost) { options.disallow_auth = false; } if (!validator.isURL(trimmed, options)) { errors.push('Invalid URL format'); } // Additional security checks if (trimmed.includes('javascript:')) { errors.push('URL contains invalid content'); } return { valid: errors.length === 0, errors, sanitized: trimmed }; } /** * Validate playlist name */ function validatePlaylistName(name) { const errors = []; const rules = VALIDATION_RULES.playlistName; if (!name || typeof name !== 'string') { errors.push('Playlist name is required'); return { valid: false, errors, sanitized: null }; } const trimmed = name.trim(); if (trimmed.length < rules.minLength) { errors.push(`Playlist name must be at least ${rules.minLength} character`); } if (trimmed.length > rules.maxLength) { errors.push(`Playlist name must not exceed ${rules.maxLength} characters`); } if (!rules.pattern.test(trimmed)) { errors.push('Playlist name contains invalid characters'); } const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed; return { valid: errors.length === 0, errors, sanitized }; } /** * Validate channel name */ function validateChannelName(name) { const errors = []; const rules = VALIDATION_RULES.channelName; if (!name || typeof name !== 'string') { errors.push('Channel name is required'); return { valid: false, errors, sanitized: null }; } const trimmed = name.trim(); if (trimmed.length < rules.minLength) { errors.push(`Channel name must be at least ${rules.minLength} character`); } if (trimmed.length > rules.maxLength) { errors.push(`Channel name must not exceed ${rules.maxLength} characters`); } const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed; return { valid: errors.length === 0, errors, sanitized }; } /** * Validate description/text field */ function validateDescription(description) { const errors = []; const rules = VALIDATION_RULES.description; if (!description) { return { valid: true, errors: [], sanitized: '' }; } if (typeof description !== 'string') { errors.push('Description must be a string'); return { valid: false, errors, sanitized: null }; } const trimmed = description.trim(); if (trimmed.length > rules.maxLength) { errors.push(`Description must not exceed ${rules.maxLength} characters`); } const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed; return { valid: errors.length === 0, errors, sanitized }; } /** * Validate filename */ function validateFilename(filename) { const errors = []; const rules = VALIDATION_RULES.filename; if (!filename || typeof filename !== 'string') { errors.push('Filename is required'); return { valid: false, errors, sanitized: null }; } const trimmed = filename.trim(); if (trimmed.length > rules.maxLength) { errors.push(`Filename must not exceed ${rules.maxLength} characters`); } if (!rules.pattern.test(trimmed)) { errors.push('Filename contains invalid characters'); } // Check for path traversal attempts if (trimmed.includes('..') || trimmed.includes('/') || trimmed.includes('\\')) { errors.push('Filename contains invalid path characters'); } const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed; return { valid: errors.length === 0, errors, sanitized }; } /** * Validate setting key */ function validateSettingKey(key) { const errors = []; const rules = VALIDATION_RULES.settingKey; if (!key || typeof key !== 'string') { errors.push('Setting key is required'); return { valid: false, errors, sanitized: null }; } const trimmed = key.trim(); if (trimmed.length > rules.maxLength) { errors.push(`Setting key must not exceed ${rules.maxLength} characters`); } if (!rules.pattern.test(trimmed)) { errors.push('Setting key contains invalid characters'); } return { valid: errors.length === 0, errors, sanitized: trimmed }; } /** * Validate integer */ function validateInteger(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) { const errors = []; const num = parseInt(value, 10); if (isNaN(num)) { errors.push('Must be a valid integer'); return { valid: false, errors, sanitized: null }; } if (num < min) { errors.push(`Must be at least ${min}`); } if (num > max) { errors.push(`Must not exceed ${max}`); } return { valid: errors.length === 0, errors, sanitized: num }; } /** * Validate boolean */ function validateBoolean(value) { if (typeof value === 'boolean') { return { valid: true, errors: [], sanitized: value }; } if (value === 'true' || value === '1' || value === 1) { return { valid: true, errors: [], sanitized: true }; } if (value === 'false' || value === '0' || value === 0) { return { valid: true, errors: [], sanitized: false }; } return { valid: false, errors: ['Must be a valid boolean'], sanitized: null }; } /** * Validate JSON */ function validateJSON(value, maxSize = 10000) { const errors = []; if (typeof value === 'object') { const jsonString = JSON.stringify(value); if (jsonString.length > maxSize) { errors.push(`JSON data exceeds maximum size of ${maxSize} characters`); } return { valid: errors.length === 0, errors, sanitized: value }; } if (typeof value !== 'string') { errors.push('Must be valid JSON'); return { valid: false, errors, sanitized: null }; } try { const parsed = JSON.parse(value); if (value.length > maxSize) { errors.push(`JSON data exceeds maximum size of ${maxSize} characters`); } return { valid: errors.length === 0, errors, sanitized: parsed }; } catch (e) { errors.push('Invalid JSON format'); return { valid: false, errors, sanitized: null }; } } /** * Sanitize object with multiple fields */ function sanitizeObject(obj, schema) { const sanitized = {}; const errors = {}; let hasErrors = false; for (const [key, validator] of Object.entries(schema)) { const value = obj[key]; const result = validator(value); if (!result.valid) { errors[key] = result.errors; hasErrors = true; } else { sanitized[key] = result.sanitized; } } return { valid: !hasErrors, errors, sanitized }; } module.exports = { validateUsername, validateEmail, validateUrl, validatePlaylistName, validateChannelName, validateDescription, validateFilename, validateSettingKey, validateInteger, validateBoolean, validateJSON, sanitizeString, sanitizeObject, VALIDATION_RULES };