Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions TUnit.Engine/Helpers/PathValidator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,57 @@
#if NET8_0_OR_GREATER
using System.Buffers;
#endif

namespace TUnit.Engine.Helpers;

internal static class PathValidator
{
#if NET8_0_OR_GREATER
private static readonly SearchValues<char> _invalidFileNameChars =
SearchValues.Create(Path.GetInvalidFileNameChars());
#endif

/// <summary>
/// Removes any characters that are invalid in a file name (e.g. path separators),
/// preventing path traversal via crafted assembly names. Equivalent to
/// <c>string.Concat(name.Split(Path.GetInvalidFileNameChars()))</c> but allocation-free
/// when the name is already clean.
/// </summary>
internal static string SanitizeFileName(string name)
{
#if NET8_0_OR_GREATER
var span = name.AsSpan();
var firstInvalid = span.IndexOfAny(_invalidFileNameChars);

// Fast path: nothing to strip, return the original string unchanged.
if (firstInvalid < 0)
{
return name;
}

// Worst case the result is the same length as the input (no chars removed
// after the first), so a single stack/heap buffer of that size suffices.
var buffer = name.Length <= 256 ? stackalloc char[name.Length] : new char[name.Length];

// Everything before the first invalid char is known-good.
span.Slice(0, firstInvalid).CopyTo(buffer);
var written = firstInvalid;

for (var i = firstInvalid; i < span.Length; i++)
{
var c = span[i];
if (!_invalidFileNameChars.Contains(c))
{
buffer[written++] = c;
}
}

return new string(buffer.Slice(0, written));
#else
return string.Concat(name.Split(Path.GetInvalidFileNameChars()));
#endif
}

/// <summary>
/// Validates and normalizes a file path to prevent path traversal attacks.
/// Returns the normalized full path if valid, or throws an <see cref="ArgumentException"/> if the path is unsafe.
Expand Down
3 changes: 2 additions & 1 deletion TUnit.Engine/Reporters/Html/HtmlReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using TUnit.Engine.Constants;
using TUnit.Engine.Exceptions;
using TUnit.Engine.Framework;
using TUnit.Engine.Helpers;

#pragma warning disable TPEXP

Expand Down Expand Up @@ -647,7 +648,7 @@ private static (string Status, ReportExceptionData? Exception, string? SkipReaso
private string GetDefaultOutputPath()
{
var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults";
var sanitizedName = string.Concat(assemblyName.Split(Path.GetInvalidFileNameChars()));
var sanitizedName = PathValidator.SanitizeFileName(assemblyName);
var os = GetShortOsName();
var tfm = GetShortFrameworkName();
return Path.GetFullPath(Path.Combine(_resultsDirectory, $"{sanitizedName}-{os}-{tfm}-report.html"));
Expand Down
2 changes: 1 addition & 1 deletion TUnit.Engine/Reporters/JUnitReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ private string GetDefaultOutputPath()
var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults";

// Sanitize assembly name to remove any characters that could be used for path traversal
var sanitizedName = string.Concat(assemblyName.Split(Path.GetInvalidFileNameChars()));
var sanitizedName = PathValidator.SanitizeFileName(assemblyName);

return Path.GetFullPath(Path.Combine(_resultsDirectory, $"{sanitizedName}-junit.xml"));
}
Expand Down
Loading