using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; namespace DatabaseSnapshotsService.Models { public static class InputValidation { // Validation patterns private static readonly Regex ValidFileNamePattern = new(@"^[a-zA-Z0-9._-]+$", RegexOptions.Compiled); private static readonly Regex ValidPathPattern = new(@"^[a-zA-Z0-9/._-]+$", RegexOptions.Compiled); private static readonly Regex ValidNamePattern = new(@"^[a-zA-Z0-9\s._-]{1,100}$", RegexOptions.Compiled); private static readonly Regex ValidDescriptionPattern = new(@"^[a-zA-Z0-9\s.,!?@#$%^&*()_+-=:;'""<>/\\|`~]{0,500}$", RegexOptions.Compiled); private static readonly Regex ValidTableNamePattern = new(@"^[a-zA-Z][a-zA-Z0-9_]*$", RegexOptions.Compiled); private static readonly Regex ValidOperationPattern = new(@"^(insert|update|delete|truncate|query)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ValidEventTypePattern = new(@"^(binlog|status|error|info)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); // Dangerous patterns to detect private static readonly Regex[] DangerousPatterns = { new(@"\.\./", RegexOptions.Compiled), // Path traversal new(@"\.\.\\", RegexOptions.Compiled), // Windows path traversal new(@"[<>""'&]", RegexOptions.Compiled), // HTML/XML injection new(@"(union|select|insert|update|delete|drop|create|alter|exec|execute|script|javascript|vbscript|onload|onerror)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // SQL/script injection new(@"(\x00|\x01|\x02|\x03|\x04|\x05|\x06|\x07|\x08|\x0B|\x0C|\x0E|\x0F|\x10|\x11|\x12|\x13|\x14|\x15|\x16|\x17|\x18|\x19|\x1A|\x1B|\x1C|\x1D|\x1E|\x1F)", RegexOptions.Compiled), // Control characters }; public static class SnapshotValidation { public static ValidationResult ValidateSnapshotName(string name) { if (string.IsNullOrWhiteSpace(name)) { return new ValidationResult("Snapshot name cannot be empty"); } if (name.Length > 100) { return new ValidationResult("Snapshot name cannot exceed 100 characters"); } if (!ValidNamePattern.IsMatch(name)) { return new ValidationResult("Snapshot name contains invalid characters. Use only letters, numbers, spaces, dots, underscores, and hyphens"); } if (ContainsDangerousPatterns(name)) { return new ValidationResult("Snapshot name contains potentially dangerous content"); } return ValidationResult.Success!; } public static ValidationResult ValidateSnapshotDescription(string? description) { if (string.IsNullOrWhiteSpace(description)) { return ValidationResult.Success!; // Description is optional } if (description.Length > 500) { return new ValidationResult("Snapshot description cannot exceed 500 characters"); } if (!ValidDescriptionPattern.IsMatch(description)) { return new ValidationResult("Snapshot description contains invalid characters"); } if (ContainsDangerousPatterns(description)) { return new ValidationResult("Snapshot description contains potentially dangerous content"); } return ValidationResult.Success!; } public static ValidationResult ValidateSnapshotId(int id) { if (id <= 0) { return new ValidationResult("Snapshot ID must be a positive integer"); } if (id > int.MaxValue) { return new ValidationResult("Snapshot ID is too large"); } return ValidationResult.Success!; } public static ValidationResult ValidateSnapshotType(string type) { if (string.IsNullOrWhiteSpace(type)) { return new ValidationResult("Snapshot type cannot be empty"); } var validTypes = new[] { "full", "incremental", "trading", "user" }; if (!validTypes.Contains(type.ToLowerInvariant())) { return new ValidationResult($"Invalid snapshot type. Must be one of: {string.Join(", ", validTypes)}"); } return ValidationResult.Success!; } } public static class EventValidation { public static ValidationResult ValidateTableName(string tableName) { if (string.IsNullOrWhiteSpace(tableName)) { return new ValidationResult("Table name cannot be empty"); } if (tableName.Length > 64) { return new ValidationResult("Table name cannot exceed 64 characters"); } if (!ValidTableNamePattern.IsMatch(tableName)) { return new ValidationResult("Table name contains invalid characters. Use only letters, numbers, and underscores, starting with a letter"); } if (ContainsDangerousPatterns(tableName)) { return new ValidationResult("Table name contains potentially dangerous content"); } return ValidationResult.Success!; } public static ValidationResult ValidateOperation(string operation) { if (string.IsNullOrWhiteSpace(operation)) { return new ValidationResult("Operation cannot be empty"); } if (!ValidOperationPattern.IsMatch(operation)) { return new ValidationResult("Invalid operation. Must be one of: insert, update, delete, truncate, query"); } return ValidationResult.Success!; } public static ValidationResult ValidateEventType(string eventType) { if (string.IsNullOrWhiteSpace(eventType)) { return new ValidationResult("Event type cannot be empty"); } if (!ValidEventTypePattern.IsMatch(eventType)) { return new ValidationResult("Invalid event type. Must be one of: binlog, status, error, info"); } return ValidationResult.Success!; } public static ValidationResult ValidateEventData(string data) { if (string.IsNullOrWhiteSpace(data)) { return new ValidationResult("Event data cannot be empty"); } if (data.Length > 10000) // 10KB limit { return new ValidationResult("Event data cannot exceed 10KB"); } if (ContainsDangerousPatterns(data)) { return new ValidationResult("Event data contains potentially dangerous content"); } return ValidationResult.Success!; } public static ValidationResult ValidateLimit(int limit) { if (limit <= 0) { return new ValidationResult("Limit must be a positive integer"); } if (limit > 10000) { return new ValidationResult("Limit cannot exceed 10000"); } return ValidationResult.Success!; } public static ValidationResult ValidateTimestamp(long timestamp) { if (timestamp < 0) { return new ValidationResult("Timestamp cannot be negative"); } var maxTimestamp = DateTimeOffset.MaxValue.ToUnixTimeSeconds(); if (timestamp > maxTimestamp) { return new ValidationResult("Timestamp is too far in the future"); } return ValidationResult.Success!; } } public static class RecoveryValidation { public static ValidationResult ValidateRecoveryPointName(string name) { if (string.IsNullOrWhiteSpace(name)) { return new ValidationResult("Recovery point name cannot be empty"); } if (name.Length > 100) { return new ValidationResult("Recovery point name cannot exceed 100 characters"); } if (!ValidNamePattern.IsMatch(name)) { return new ValidationResult("Recovery point name contains invalid characters. Use only letters, numbers, spaces, dots, underscores, and hyphens"); } if (ContainsDangerousPatterns(name)) { return new ValidationResult("Recovery point name contains potentially dangerous content"); } return ValidationResult.Success!; } public static ValidationResult ValidateRecoveryPointDescription(string? description) { if (string.IsNullOrWhiteSpace(description)) { return ValidationResult.Success!; // Description is optional } if (description.Length > 500) { return new ValidationResult("Recovery point description cannot exceed 500 characters"); } if (!ValidDescriptionPattern.IsMatch(description)) { return new ValidationResult("Recovery point description contains invalid characters"); } if (ContainsDangerousPatterns(description)) { return new ValidationResult("Recovery point description contains potentially dangerous content"); } return ValidationResult.Success!; } } public static class FileValidation { public static ValidationResult ValidateFilePath(string filePath) { if (string.IsNullOrWhiteSpace(filePath)) { return new ValidationResult("File path cannot be empty"); } if (filePath.Length > 260) // Windows path limit { return new ValidationResult("File path cannot exceed 260 characters"); } if (!ValidPathPattern.IsMatch(filePath)) { return new ValidationResult("File path contains invalid characters"); } if (ContainsDangerousPatterns(filePath)) { return new ValidationResult("File path contains potentially dangerous content"); } // Check for path traversal attempts var normalizedPath = Path.GetFullPath(filePath); if (normalizedPath.Contains("..") || normalizedPath.Contains("~")) { return new ValidationResult("File path contains invalid path traversal characters"); } return ValidationResult.Success!; } public static ValidationResult ValidateFileName(string fileName) { if (string.IsNullOrWhiteSpace(fileName)) { return new ValidationResult("File name cannot be empty"); } if (fileName.Length > 255) { return new ValidationResult("File name cannot exceed 255 characters"); } if (!ValidFileNamePattern.IsMatch(fileName)) { return new ValidationResult("File name contains invalid characters. Use only letters, numbers, dots, underscores, and hyphens"); } if (ContainsDangerousPatterns(fileName)) { return new ValidationResult("File name contains potentially dangerous content"); } // Check for reserved Windows filenames var reservedNames = new[] { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName).ToUpperInvariant(); if (reservedNames.Contains(fileNameWithoutExtension)) { return new ValidationResult("File name is a reserved system name"); } return ValidationResult.Success!; } } public static class DataValidation { public static ValidationResult ValidateDataContains(string dataContains) { if (string.IsNullOrWhiteSpace(dataContains)) { return new ValidationResult("Data contains filter cannot be empty"); } if (dataContains.Length > 1000) { return new ValidationResult("Data contains filter cannot exceed 1000 characters"); } if (ContainsDangerousPatterns(dataContains)) { return new ValidationResult("Data contains filter contains potentially dangerous content"); } return ValidationResult.Success!; } public static ValidationResult ValidateOutputFile(string outputFile) { if (string.IsNullOrWhiteSpace(outputFile)) { return new ValidationResult("Output file path cannot be empty"); } return FileValidation.ValidateFilePath(outputFile); } } private static bool ContainsDangerousPatterns(string input) { return DangerousPatterns.Any(pattern => pattern.IsMatch(input)); } public static string SanitizeString(string input) { if (string.IsNullOrWhiteSpace(input)) { return string.Empty; } // Remove control characters var sanitized = Regex.Replace(input, @"[\x00-\x1F\x7F]", ""); // Trim whitespace sanitized = sanitized.Trim(); return sanitized; } public static string SanitizeFileName(string fileName) { if (string.IsNullOrWhiteSpace(fileName)) { return string.Empty; } // Remove invalid characters for file names var sanitized = Regex.Replace(fileName, @"[<>:""/\\|?*\x00-\x1F]", ""); // Trim and limit length sanitized = sanitized.Trim(); if (sanitized.Length > 255) { sanitized = sanitized.Substring(0, 255); } return sanitized; } public static string SanitizePath(string path) { if (string.IsNullOrWhiteSpace(path)) { return string.Empty; } // Remove path traversal attempts var sanitized = path.Replace("..", "").Replace("~", ""); // Remove invalid characters sanitized = Regex.Replace(sanitized, @"[<>""|?\x00-\x1F]", ""); // Normalize path separators sanitized = sanitized.Replace('\\', '/'); return sanitized; } } }