438 lines
16 KiB
C#
438 lines
16 KiB
C#
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;
|
|
}
|
|
}
|
|
} |