Files
WCTDataMiner/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Security/PathValidator.cs
Scottxjw a3100c73c5 refactor(core): 重构核心分层结构并添加发布库聚合
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-13 15:00:39 +08:00

157 lines
5.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.RegularExpressions;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
/// <summary>
/// 路径验证结果
/// </summary>
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);
}
/// <summary>
/// 路径验证器 - 防止路径遍历攻击
/// </summary>
public class PathValidator
{
private readonly string _applicationBasePath;
private readonly HashSet<string> _allowedDirectories;
/// <summary>
/// 默认允许的输出目录
/// </summary>
private static readonly string[] DefaultAllowedDirectories = ["exports", "output", "data/exports", "data/output"];
public PathValidator(string applicationBasePath, IEnumerable<string>? allowedDirectories = null)
{
_applicationBasePath = Path.GetFullPath(applicationBasePath);
_allowedDirectories = new HashSet<string>(
(allowedDirectories ?? DefaultAllowedDirectories).Select(d => d.Replace('/', Path.DirectorySeparatorChar)),
StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// 验证输出目录路径
/// </summary>
/// <param name="outputDir">用户输入的输出目录路径</param>
/// <returns>验证结果</returns>
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);
}
/// <summary>
/// 简化版验证 - 仅检查路径安全性,不做白名单检查
/// </summary>
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);
}
}