336 lines
13 KiB
C#
Executable File
336 lines
13 KiB
C#
Executable File
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace MarketAlly.AIPlugin.Refactoring.Security
|
|
{
|
|
public static class SecurePathValidator
|
|
{
|
|
private static readonly string[] ForbiddenPaths = new[]
|
|
{
|
|
"program files", "programdata",
|
|
"boot", "etc", "bin", "sbin", "usr", "var"
|
|
};
|
|
|
|
private static readonly string[] SystemDirectories = new[]
|
|
{
|
|
"windows\\system32", "windows\\system", "windows\\syswow64"
|
|
};
|
|
|
|
private static readonly string[] DangerousExtensions = new[]
|
|
{
|
|
".exe", ".dll", ".com", ".bat", ".cmd", ".scr", ".vbs", ".js",
|
|
".jar", ".msi", ".ps1", ".psm1", ".psd1", ".sys", ".inf"
|
|
};
|
|
|
|
private static readonly Regex UnsafePathChars = new(
|
|
@"[<>""|?*\x00-\x1f]|(\.\./)|(\.\.)\\",
|
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
|
|
private static readonly Regex ReservedNames = new(
|
|
@"^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)",
|
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
|
|
/// <summary>
|
|
/// Validates and normalizes a file path to prevent path traversal attacks
|
|
/// </summary>
|
|
/// <param name="inputPath">The input path to validate</param>
|
|
/// <param name="basePath">The base directory that the path should be contained within</param>
|
|
/// <returns>The normalized and validated full path</returns>
|
|
/// <exception cref="SecurityException">Thrown when path validation fails</exception>
|
|
/// <exception cref="ArgumentException">Thrown when input parameters are invalid</exception>
|
|
public static string ValidateAndNormalizePath(string inputPath, string basePath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(inputPath))
|
|
throw new ArgumentException("Input path cannot be null or empty", nameof(inputPath));
|
|
|
|
if (string.IsNullOrWhiteSpace(basePath))
|
|
throw new ArgumentException("Base path cannot be null or empty", nameof(basePath));
|
|
|
|
// Remove any unsafe characters first
|
|
ValidatePathCharacters(inputPath);
|
|
|
|
try
|
|
{
|
|
// Normalize the base path
|
|
var normalizedBasePath = Path.GetFullPath(basePath);
|
|
|
|
// Combine and get full path to resolve any relative path components
|
|
var combinedPath = Path.Combine(normalizedBasePath, inputPath);
|
|
var fullPath = Path.GetFullPath(combinedPath);
|
|
|
|
// Ensure the resolved path is still within the base directory
|
|
if (!IsPathWithinBase(fullPath, normalizedBasePath))
|
|
{
|
|
throw new SecurityException($"Path traversal attempt detected: '{inputPath}' resolves outside base directory '{basePath}'");
|
|
}
|
|
|
|
// Additional security checks
|
|
ValidatePathSafety(fullPath);
|
|
|
|
return fullPath;
|
|
}
|
|
catch (Exception ex) when (!(ex is SecurityException))
|
|
{
|
|
throw new SecurityException($"Invalid path format: '{inputPath}'", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates a path without requiring a base directory
|
|
/// </summary>
|
|
/// <param name="path">The path to validate</param>
|
|
/// <returns>The normalized full path</returns>
|
|
/// <exception cref="SecurityException">Thrown when path validation fails</exception>
|
|
public static string ValidatePath(string path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
throw new ArgumentException("Path cannot be null or empty", nameof(path));
|
|
|
|
ValidatePathCharacters(path);
|
|
|
|
try
|
|
{
|
|
var fullPath = Path.GetFullPath(path);
|
|
ValidatePathSafety(fullPath);
|
|
return fullPath;
|
|
}
|
|
catch (Exception ex) when (!(ex is SecurityException))
|
|
{
|
|
throw new SecurityException($"Invalid path format: '{path}'", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that a file path is safe for code analysis operations
|
|
/// </summary>
|
|
/// <param name="filePath">The file path to validate</param>
|
|
/// <returns>True if the file is safe to analyze</returns>
|
|
public static bool IsFilePathSafeForAnalysis(string filePath)
|
|
{
|
|
try
|
|
{
|
|
var validatedPath = ValidatePath(filePath);
|
|
var extension = Path.GetExtension(validatedPath).ToLowerInvariant();
|
|
|
|
// Only allow specific file types for analysis
|
|
var allowedExtensions = new[] { ".cs", ".csx", ".txt", ".md", ".json", ".xml", ".xaml" };
|
|
|
|
if (!allowedExtensions.Contains(extension))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if it's in a safe directory
|
|
return !IsInDangerousDirectory(validatedPath);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a safe file name by removing or replacing dangerous characters
|
|
/// </summary>
|
|
/// <param name="fileName">The original file name</param>
|
|
/// <returns>A sanitized file name</returns>
|
|
public static string CreateSafeFileName(string fileName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(fileName))
|
|
throw new ArgumentException("File name cannot be null or empty", nameof(fileName));
|
|
|
|
var invalidChars = Path.GetInvalidFileNameChars();
|
|
var safeName = string.Concat(fileName.Where(c => !invalidChars.Contains(c)));
|
|
|
|
// Replace multiple spaces with single space
|
|
safeName = Regex.Replace(safeName, @"\s+", " ");
|
|
|
|
// Trim and limit length
|
|
safeName = safeName.Trim();
|
|
if (safeName.Length > 200)
|
|
{
|
|
var extension = Path.GetExtension(safeName);
|
|
var nameWithoutExt = Path.GetFileNameWithoutExtension(safeName);
|
|
safeName = nameWithoutExt.Substring(0, 200 - extension.Length) + extension;
|
|
}
|
|
|
|
// Check for reserved names
|
|
if (ReservedNames.IsMatch(safeName))
|
|
{
|
|
safeName = "_" + safeName;
|
|
}
|
|
|
|
return string.IsNullOrWhiteSpace(safeName) ? "safe_file" : safeName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates directory path for safe operations
|
|
/// </summary>
|
|
/// <param name="directoryPath">The directory path to validate</param>
|
|
/// <param name="basePath">Optional base path for containment checking</param>
|
|
/// <returns>The validated directory path</returns>
|
|
public static string ValidateDirectoryPath(string directoryPath, string? basePath = null)
|
|
{
|
|
var validatedPath = basePath != null
|
|
? ValidateAndNormalizePath(directoryPath, basePath)
|
|
: ValidatePath(directoryPath);
|
|
|
|
if (IsInDangerousDirectory(validatedPath))
|
|
{
|
|
throw new SecurityException($"Directory path is in a dangerous location: '{directoryPath}'");
|
|
}
|
|
|
|
return validatedPath;
|
|
}
|
|
|
|
private static void ValidatePathCharacters(string path)
|
|
{
|
|
if (UnsafePathChars.IsMatch(path))
|
|
{
|
|
throw new SecurityException($"Path contains unsafe characters: '{path}'");
|
|
}
|
|
|
|
// Check for null bytes (additional security)
|
|
if (path.Contains('\0'))
|
|
{
|
|
throw new SecurityException("Path contains null bytes");
|
|
}
|
|
|
|
// Check for excessively long paths
|
|
if (path.Length > 32767) // Max path length on Windows
|
|
{
|
|
throw new SecurityException("Path is too long");
|
|
}
|
|
}
|
|
|
|
private static bool IsPathWithinBase(string fullPath, string basePath)
|
|
{
|
|
// Normalize both paths for comparison
|
|
var normalizedFullPath = fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
var normalizedBasePath = basePath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
|
|
return normalizedFullPath.StartsWith(normalizedBasePath, StringComparison.OrdinalIgnoreCase) &&
|
|
(normalizedFullPath.Length == normalizedBasePath.Length ||
|
|
normalizedFullPath[normalizedBasePath.Length] == Path.DirectorySeparatorChar ||
|
|
normalizedFullPath[normalizedBasePath.Length] == Path.AltDirectorySeparatorChar);
|
|
}
|
|
|
|
private static void ValidatePathSafety(string fullPath)
|
|
{
|
|
var fileName = Path.GetFileName(fullPath);
|
|
var extension = Path.GetExtension(fullPath).ToLowerInvariant();
|
|
|
|
// Check for dangerous file extensions
|
|
if (DangerousExtensions.Contains(extension))
|
|
{
|
|
throw new SecurityException($"File extension '{extension}' is not allowed for security reasons");
|
|
}
|
|
|
|
// Check for reserved file names
|
|
if (ReservedNames.IsMatch(fileName))
|
|
{
|
|
throw new SecurityException($"File name '{fileName}' is a reserved system name");
|
|
}
|
|
|
|
// Check if it's in a dangerous directory
|
|
if (IsInDangerousDirectory(fullPath))
|
|
{
|
|
throw new SecurityException($"Path is in a dangerous system directory: '{fullPath}'");
|
|
}
|
|
}
|
|
|
|
private static bool IsInDangerousDirectory(string fullPath)
|
|
{
|
|
var normalizedPath = fullPath.ToLowerInvariant().Replace('/', '\\');
|
|
|
|
// Check for specific system directories
|
|
foreach (var systemDir in SystemDirectories)
|
|
{
|
|
if (normalizedPath.Contains(systemDir, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var pathParts = normalizedPath
|
|
.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
|
.Where(part => !string.IsNullOrEmpty(part))
|
|
.ToArray();
|
|
|
|
// Check if any part of the path matches forbidden directories
|
|
return pathParts.Any(part => ForbiddenPaths.Any(forbidden =>
|
|
part.Equals(forbidden, StringComparison.OrdinalIgnoreCase) ||
|
|
part.StartsWith(forbidden, StringComparison.OrdinalIgnoreCase)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the relative path from base to target, ensuring it's safe
|
|
/// </summary>
|
|
/// <param name="basePath">The base directory path</param>
|
|
/// <param name="targetPath">The target file path</param>
|
|
/// <returns>Safe relative path</returns>
|
|
public static string GetSafeRelativePath(string basePath, string targetPath)
|
|
{
|
|
var validatedBasePath = ValidatePath(basePath);
|
|
var validatedTargetPath = ValidateAndNormalizePath(targetPath, validatedBasePath);
|
|
|
|
try
|
|
{
|
|
return Path.GetRelativePath(validatedBasePath, validatedTargetPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new SecurityException($"Cannot create safe relative path from '{basePath}' to '{targetPath}'", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that a path is safe for temporary file operations
|
|
/// </summary>
|
|
/// <param name="tempPath">The temporary path to validate</param>
|
|
/// <returns>Validated temporary path</returns>
|
|
public static string ValidateTempPath(string tempPath)
|
|
{
|
|
var systemTempPath = Path.GetTempPath();
|
|
var validatedPath = ValidateAndNormalizePath(tempPath, systemTempPath);
|
|
|
|
// Additional validation for temp files
|
|
var fileName = Path.GetFileName(validatedPath);
|
|
if (fileName.StartsWith(".", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new SecurityException("Temporary file names cannot start with '.'");
|
|
}
|
|
|
|
return validatedPath;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extension methods for easy path validation
|
|
/// </summary>
|
|
public static class PathValidationExtensions
|
|
{
|
|
public static string ValidateAsSecurePath(this string path)
|
|
{
|
|
return SecurePathValidator.ValidatePath(path);
|
|
}
|
|
|
|
public static string ValidateAsSecurePath(this string path, string basePath)
|
|
{
|
|
return SecurePathValidator.ValidateAndNormalizePath(path, basePath);
|
|
}
|
|
|
|
public static bool IsSecureForAnalysis(this string filePath)
|
|
{
|
|
return SecurePathValidator.IsFilePathSafeForAnalysis(filePath);
|
|
}
|
|
|
|
public static string ToSafeFileName(this string fileName)
|
|
{
|
|
return SecurePathValidator.CreateSafeFileName(fileName);
|
|
}
|
|
}
|
|
} |