279 lines
10 KiB
C#
Executable File
279 lines
10 KiB
C#
Executable File
using Microsoft.Extensions.Logging;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace MarketAlly.AIPlugin.Analysis.Infrastructure
|
|
{
|
|
/// <summary>
|
|
/// Input validation and security service for analysis operations
|
|
/// </summary>
|
|
public class InputValidator
|
|
{
|
|
private readonly ILogger<InputValidator>? _logger;
|
|
private static readonly HashSet<string> AllowedFileExtensions = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
".cs", ".csproj", ".sln", ".json", ".xml", ".config", ".md", ".txt",
|
|
".dll", ".exe", ".pdb", ".nuspec", ".props", ".targets"
|
|
};
|
|
|
|
private static readonly Regex SafePathRegex = new(@"^[a-zA-Z0-9\\\/:._\-\s]+$", RegexOptions.Compiled);
|
|
private static readonly Regex DangerousPatternRegex = new(@"(\.\.[\\/]|<script|javascript:|vbscript:|onload=|onerror=)",
|
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
|
|
public InputValidator(ILogger<InputValidator>? logger = null)
|
|
{
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates and sanitizes a file path
|
|
/// </summary>
|
|
public ValidationResult ValidateFilePath(string? filePath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(filePath))
|
|
{
|
|
return ValidationResult.Failure("File path cannot be null or empty");
|
|
}
|
|
|
|
// Check for dangerous patterns
|
|
if (DangerousPatternRegex.IsMatch(filePath))
|
|
{
|
|
_logger?.LogWarning("Dangerous pattern detected in file path: {FilePath}", filePath);
|
|
return ValidationResult.Failure("File path contains potentially dangerous patterns");
|
|
}
|
|
|
|
// Validate path format
|
|
if (!SafePathRegex.IsMatch(filePath))
|
|
{
|
|
return ValidationResult.Failure("File path contains invalid characters");
|
|
}
|
|
|
|
// Check for path traversal attempts
|
|
var normalizedPath = Path.GetFullPath(filePath);
|
|
if (filePath.Contains("..") && !normalizedPath.StartsWith(Path.GetFullPath(".")))
|
|
{
|
|
_logger?.LogWarning("Path traversal attempt detected: {FilePath}", filePath);
|
|
return ValidationResult.Failure("Path traversal detected");
|
|
}
|
|
|
|
// Validate file extension
|
|
var extension = Path.GetExtension(filePath);
|
|
if (!string.IsNullOrEmpty(extension) && !AllowedFileExtensions.Contains(extension))
|
|
{
|
|
return ValidationResult.Failure($"File extension '{extension}' is not allowed");
|
|
}
|
|
|
|
return ValidationResult.Success(normalizedPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates plugin parameters for security issues
|
|
/// </summary>
|
|
public ValidationResult ValidatePluginParameters(Dictionary<string, object>? parameters)
|
|
{
|
|
if (parameters == null)
|
|
{
|
|
return ValidationResult.Success();
|
|
}
|
|
|
|
foreach (var kvp in parameters)
|
|
{
|
|
// Validate parameter name
|
|
if (string.IsNullOrWhiteSpace(kvp.Key) || !IsValidParameterName(kvp.Key))
|
|
{
|
|
return ValidationResult.Failure($"Invalid parameter name: {kvp.Key}");
|
|
}
|
|
|
|
// Validate parameter value
|
|
var valueValidation = ValidateParameterValue(kvp.Key, kvp.Value);
|
|
if (!valueValidation.IsValid)
|
|
{
|
|
return valueValidation;
|
|
}
|
|
}
|
|
|
|
return ValidationResult.Success();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates analysis configuration settings
|
|
/// </summary>
|
|
public ValidationResult ValidateConfiguration(AnalysisConfiguration? config)
|
|
{
|
|
if (config == null)
|
|
{
|
|
return ValidationResult.Failure("Configuration cannot be null");
|
|
}
|
|
|
|
// Validate timeout values
|
|
if (config.DefaultTimeout <= TimeSpan.Zero || config.DefaultTimeout > TimeSpan.FromHours(1))
|
|
{
|
|
return ValidationResult.Failure("Default timeout must be between 1 second and 1 hour");
|
|
}
|
|
|
|
// Validate concurrency limits
|
|
if (config.MaxConcurrentAnalyses < 1 || config.MaxConcurrentAnalyses > Environment.ProcessorCount * 4)
|
|
{
|
|
return ValidationResult.Failure($"Max concurrent analyses must be between 1 and {Environment.ProcessorCount * 4}");
|
|
}
|
|
|
|
// Validate cache settings
|
|
if (config.CacheExpirationTime < TimeSpan.FromMinutes(1) || config.CacheExpirationTime > TimeSpan.FromDays(7))
|
|
{
|
|
return ValidationResult.Failure("Cache expiration time must be between 1 minute and 7 days");
|
|
}
|
|
|
|
// Validate security settings
|
|
if (config.AllowDynamicPluginLoading && string.IsNullOrWhiteSpace(config.TrustedPluginDirectory))
|
|
{
|
|
return ValidationResult.Failure("Trusted plugin directory must be specified when dynamic plugin loading is enabled");
|
|
}
|
|
|
|
return ValidationResult.Success();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sanitizes string input to remove potentially dangerous content
|
|
/// </summary>
|
|
public string SanitizeInput(string? input)
|
|
{
|
|
if (string.IsNullOrEmpty(input))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
// Remove or escape potentially dangerous characters
|
|
var sanitized = input
|
|
.Replace("<", "<")
|
|
.Replace(">", ">")
|
|
.Replace("&", "&")
|
|
.Replace("\"", """)
|
|
.Replace("'", "'")
|
|
.Replace("/", "/");
|
|
|
|
// Remove null bytes and control characters (except common whitespace)
|
|
sanitized = Regex.Replace(sanitized, @"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "");
|
|
|
|
return sanitized.Trim();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that a directory path is safe and accessible
|
|
/// </summary>
|
|
public ValidationResult ValidateDirectoryPath(string? directoryPath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(directoryPath))
|
|
{
|
|
return ValidationResult.Failure("Directory path cannot be null or empty");
|
|
}
|
|
|
|
var pathValidation = ValidateFilePath(directoryPath);
|
|
if (!pathValidation.IsValid)
|
|
{
|
|
return pathValidation;
|
|
}
|
|
|
|
try
|
|
{
|
|
var fullPath = Path.GetFullPath(directoryPath);
|
|
|
|
// Check if directory exists and is accessible
|
|
if (!Directory.Exists(fullPath))
|
|
{
|
|
return ValidationResult.Failure("Directory does not exist");
|
|
}
|
|
|
|
// Basic permission check
|
|
try
|
|
{
|
|
Directory.GetFiles(fullPath, "*", SearchOption.TopDirectoryOnly);
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
_logger?.LogWarning("Access denied to directory: {DirectoryPath}", fullPath);
|
|
return ValidationResult.Failure("Access denied to directory");
|
|
}
|
|
|
|
return ValidationResult.Success(fullPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError(ex, "Error validating directory path: {DirectoryPath}", directoryPath);
|
|
return ValidationResult.Failure("Invalid directory path");
|
|
}
|
|
}
|
|
|
|
private bool IsValidParameterName(string parameterName)
|
|
{
|
|
// Parameter names should only contain alphanumeric characters, underscores, and dots
|
|
return Regex.IsMatch(parameterName, @"^[a-zA-Z0-9_\.]+$");
|
|
}
|
|
|
|
private ValidationResult ValidateParameterValue(string parameterName, object? value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
return ValidationResult.Success();
|
|
}
|
|
|
|
// Check for potentially dangerous string values
|
|
if (value is string stringValue)
|
|
{
|
|
if (DangerousPatternRegex.IsMatch(stringValue))
|
|
{
|
|
_logger?.LogWarning("Dangerous pattern detected in parameter {ParameterName}: {Value}",
|
|
parameterName, stringValue);
|
|
return ValidationResult.Failure($"Parameter '{parameterName}' contains potentially dangerous content");
|
|
}
|
|
|
|
// Check string length limits
|
|
if (stringValue.Length > 10000)
|
|
{
|
|
return ValidationResult.Failure($"Parameter '{parameterName}' exceeds maximum length (10000 characters)");
|
|
}
|
|
}
|
|
|
|
// Validate file paths in parameters
|
|
if (parameterName.EndsWith("Path", StringComparison.OrdinalIgnoreCase) && value is string pathValue)
|
|
{
|
|
var pathValidation = ValidateFilePath(pathValue);
|
|
if (!pathValidation.IsValid)
|
|
{
|
|
return ValidationResult.Failure($"Invalid file path in parameter '{parameterName}': {pathValidation.ErrorMessage}");
|
|
}
|
|
}
|
|
|
|
return ValidationResult.Success();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of input validation operation
|
|
/// </summary>
|
|
public class ValidationResult
|
|
{
|
|
public bool IsValid { get; private set; }
|
|
public string? ErrorMessage { get; private set; }
|
|
public string? SanitizedValue { get; private set; }
|
|
|
|
private ValidationResult(bool isValid, string? errorMessage = null, string? sanitizedValue = null)
|
|
{
|
|
IsValid = isValid;
|
|
ErrorMessage = errorMessage;
|
|
SanitizedValue = sanitizedValue;
|
|
}
|
|
|
|
public static ValidationResult Success(string? sanitizedValue = null)
|
|
{
|
|
return new ValidationResult(true, sanitizedValue: sanitizedValue);
|
|
}
|
|
|
|
public static ValidationResult Failure(string errorMessage)
|
|
{
|
|
return new ValidationResult(false, errorMessage);
|
|
}
|
|
}
|
|
} |