157 lines
5.3 KiB
C#
157 lines
5.3 KiB
C#
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);
|
||
}
|
||
} |