| File: Utilities\PathValidation.cs | |
| Project: ..\..\..\src\Libraries\Microsoft.Extensions.AI.Evaluation.Reporting\CSharp\Microsoft.Extensions.AI.Evaluation.Reporting.csproj (Microsoft.Extensions.AI.Evaluation.Reporting) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using Microsoft.Shared.Diagnostics; #if NET using System.Runtime.InteropServices; #endif namespace Microsoft.Extensions.AI.Evaluation.Reporting.Utilities; internal static class PathValidation { private static readonly char[] _invalidFileNameChars = Path.GetInvalidFileNameChars(); #pragma warning disable CA1802 // Use literals where appropriate private static readonly StringComparison _pathComparison = #if NET // Windows paths are case-insensitive; Linux/macOS paths are case-sensitive. RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; #else StringComparison.OrdinalIgnoreCase; // .NET Framework and .NET Standard only run on Windows #endif #pragma warning restore CA1802 // Use literals where appropriate /// <summary> /// Validates that a path segment is a safe single directory or file name. /// Throws <see cref="ArgumentException"/> if the segment contains path separators, /// invalid file name characters, or directory traversal sequences. /// </summary> internal static void ValidatePathSegment(string? segment, string paramName) { if (segment is null) { return; } if (segment.Length == 0 || segment != segment.Trim() || segment is "." || segment is ".." || segment.IndexOfAny(_invalidFileNameChars) >= 0) { Throw.ArgumentException( paramName, $"The parameter '{paramName}' contains invalid path characters or directory traversal sequences."); } } /// <summary> /// Verifies that a fully resolved path is contained within the specified root directory. /// Both paths are canonicalized via <see cref="Path.GetFullPath(string)"/> before comparison. /// Throws <see cref="InvalidOperationException"/> if the resolved path escapes the root. /// </summary> internal static string EnsureWithinRoot(string rootPath, string resolvedPath) { string fullRoot = Path.GetFullPath(rootPath); string normalizedRoot = fullRoot; string fullResolved = Path.GetFullPath(resolvedPath); // Ensure the root ends with a directory separator so that a root of // "/foo/bar" does not match a path like "/foo/bar-sibling/file". #if NET if (!normalizedRoot.EndsWith(Path.DirectorySeparatorChar) && !normalizedRoot.EndsWith(Path.AltDirectorySeparatorChar)) #else if (!normalizedRoot.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) && !normalizedRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal)) #endif { normalizedRoot += Path.DirectorySeparatorChar; } if (!fullResolved.StartsWith(normalizedRoot, _pathComparison) && !string.Equals(fullRoot, fullResolved, _pathComparison)) { throw new InvalidOperationException( "The resolved path escapes the configured root directory. " + "This may indicate a path traversal attempt."); } return fullResolved; } }