using System.Text.RegularExpressions; namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security; /// /// 路径验证结果 /// public class PathValidationResult { public bool IsValid { get; } public string? NormalizedPath { get; } public string? ErrorMessage { get; } private PathValidationResult(bool isValid, string? normalizedPath, string? errorMessage) { IsValid = isValid; NormalizedPath = normalizedPath; ErrorMessage = errorMessage; } public static PathValidationResult Success(string normalizedPath) => new(true, normalizedPath, null); public static PathValidationResult Fail(string errorMessage) => new(false, null, errorMessage); } /// /// 路径验证器 - 防止路径遍历攻击 /// public class PathValidator { private readonly string _applicationBasePath; private readonly HashSet _allowedDirectories; /// /// 默认允许的输出目录 /// private static readonly string[] DefaultAllowedDirectories = ["exports", "output", "data/exports", "data/output"]; public PathValidator(string applicationBasePath, IEnumerable? allowedDirectories = null) { _applicationBasePath = Path.GetFullPath(applicationBasePath); _allowedDirectories = new HashSet( (allowedDirectories ?? DefaultAllowedDirectories).Select(d => d.Replace('/', Path.DirectorySeparatorChar)), StringComparer.OrdinalIgnoreCase); } /// /// 验证输出目录路径 /// /// 用户输入的输出目录路径 /// 验证结果 public PathValidationResult ValidateOutputDirectory(string outputDir) { // 1. 基础验证 if (string.IsNullOrWhiteSpace(outputDir)) return PathValidationResult.Fail("输出目录不能为空"); // 2. 禁止的模式检查 var forbiddenPatterns = new[] { "..", // 路径遍历 "~", // 用户主目录 "\\\\", // UNC 路径 }; foreach (var pattern in forbiddenPatterns) { if (outputDir.Contains(pattern)) { return PathValidationResult.Fail($"路径包含禁止的模式: {pattern}"); } } // 3. 检查是否为绝对路径(Windows 和 Unix) if (Path.IsPathRooted(outputDir) || Regex.IsMatch(outputDir, @"^[A-Za-z]:") || // Windows 驱动器 outputDir.StartsWith("/")) // Unix 绝对路径 { return PathValidationResult.Fail("仅允许相对路径"); } // 4. 规范化并验证 string normalizedPath; try { normalizedPath = Path.GetFullPath(Path.Combine(_applicationBasePath, outputDir)); } catch (Exception ex) { return PathValidationResult.Fail($"路径格式无效: {ex.Message}"); } // 5. 严格验证必须在应用目录内 if (!normalizedPath.StartsWith(_applicationBasePath, StringComparison.OrdinalIgnoreCase)) { return PathValidationResult.Fail("路径必须在应用程序目录内"); } // 6. 白名单检查 var relative = normalizedPath .Substring(_applicationBasePath.Length) .TrimStart(Path.DirectorySeparatorChar); bool inAllowedDir = _allowedDirectories .Any(allowed => relative.StartsWith(allowed + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) || string.Equals(relative, allowed, StringComparison.OrdinalIgnoreCase)); if (!inAllowedDir) { var allowedList = string.Join(", ", _allowedDirectories); return PathValidationResult.Fail($"输出目录必须在允许列表中: {allowedList}"); } return PathValidationResult.Success(normalizedPath); } /// /// 简化版验证 - 仅检查路径安全性,不做白名单检查 /// public PathValidationResult ValidatePathSafety(string path) { if (string.IsNullOrWhiteSpace(path)) return PathValidationResult.Fail("路径不能为空"); // 检查路径遍历 if (path.Contains("..")) return PathValidationResult.Fail("路径包含非法的遍历字符"); // 检查绝对路径 if (Path.IsPathRooted(path) || Regex.IsMatch(path, @"^[A-Za-z]:") || path.StartsWith("/")) { return PathValidationResult.Fail("仅允许相对路径"); } // 检查 UNC 路径 if (path.StartsWith("\\\\")) return PathValidationResult.Fail("不允许 UNC 路径"); // 规范化验证 try { var normalized = Path.GetFullPath(Path.Combine(_applicationBasePath, path)); if (!normalized.StartsWith(_applicationBasePath, StringComparison.OrdinalIgnoreCase)) return PathValidationResult.Fail("路径超出应用程序目录范围"); } catch (Exception ex) { return PathValidationResult.Fail($"路径格式无效: {ex.Message}"); } return PathValidationResult.Success(path); } }