diff --git a/Knossos.NET/Classes/KnUtils.cs b/Knossos.NET/Classes/KnUtils.cs
index 175c047e..bbe372de 100644
--- a/Knossos.NET/Classes/KnUtils.cs
+++ b/Knossos.NET/Classes/KnUtils.cs
@@ -629,6 +629,28 @@ public static string CmdLineBuilder(string cmdline, string[]? args)
return cmdline;
}
+ ///
+ /// Returns true if candidatePath resolves to a location within basePath.
+ /// Rejects absolute paths, null bytes, and path traversal sequences such as "..".
+ ///
+ public static bool IsSubPath(string basePath, string candidatePath)
+ {
+ if (string.IsNullOrEmpty(candidatePath))
+ return true;
+ if (Path.IsPathRooted(candidatePath))
+ return false;
+ try
+ {
+ var fullBase = Path.GetFullPath(basePath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
+ var fullTarget = Path.GetFullPath(Path.Combine(basePath, candidatePath));
+ return fullTarget.StartsWith(fullBase, StringComparison.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
///
/// Gets the complete size of all files in a folder and subdirectories in bytes
///
@@ -1009,7 +1031,24 @@ private static bool DecompressFileSharpCompress(string compressedFilePath, strin
{
if (!reader.Entry.IsDirectory)
{
- reader.WriteEntryToDirectory(destFolderPath, new ExtractionOptions() { ExtractFullPath = extractFullPath, Overwrite = true, WriteSymbolicLink = (source, target) => { File.CreateSymbolicLink(source, target); } });
+ var entryKey = reader.Entry.Key?.Replace('\\', '/').Replace('/', Path.DirectorySeparatorChar);
+ if (entryKey != null && extractFullPath && !IsSubPath(destFolderPath, entryKey))
+ {
+ Log.Add(Log.LogSeverity.Warning, "KnUtils.DecompressFileSharpCompress()", $"Skipping potentially dangerous archive entry: {reader.Entry.Key}");
+ continue;
+ }
+ reader.WriteEntryToDirectory(destFolderPath, new ExtractionOptions() { ExtractFullPath = extractFullPath, Overwrite = true, WriteSymbolicLink = (source, target) =>
+ {
+ var resolvedTarget = Path.IsPathRooted(target)
+ ? target
+ : Path.GetFullPath(Path.Combine(Path.GetDirectoryName(source)!, target));
+ if (!IsSubPath(destFolderPath, Path.GetRelativePath(destFolderPath, resolvedTarget)))
+ {
+ Log.Add(Log.LogSeverity.Warning, "KnUtils.DecompressFileSharpCompress()", $"Skipping symlink escaping destination: {source} -> {target}");
+ return;
+ }
+ File.CreateSymbolicLink(source, target);
+ }});
}
if (cancellationTokenSource!.IsCancellationRequested)
{
@@ -1028,6 +1067,12 @@ private static bool DecompressFileSharpCompress(string compressedFilePath, strin
{
if (!reader.Entry.IsDirectory)
{
+ var entryKey = reader.Entry.Key?.Replace('\\', '/').Replace('/', Path.DirectorySeparatorChar);
+ if (entryKey != null && extractFullPath && !IsSubPath(destFolderPath, entryKey))
+ {
+ Log.Add(Log.LogSeverity.Warning, "KnUtils.DecompressFileSharpCompress()", $"Skipping potentially dangerous archive entry: {reader.Entry.Key}");
+ continue;
+ }
reader.WriteEntryToDirectory(destFolderPath, new ExtractionOptions() { ExtractFullPath = extractFullPath, Overwrite = true });
}
if (cancellationTokenSource!.IsCancellationRequested)
diff --git a/Knossos.NET/ViewModels/Templates/Tasks/InstallBuild.cs b/Knossos.NET/ViewModels/Templates/Tasks/InstallBuild.cs
index 9bf3aa3c..54515fe4 100644
--- a/Knossos.NET/ViewModels/Templates/Tasks/InstallBuild.cs
+++ b/Knossos.NET/ViewModels/Templates/Tasks/InstallBuild.cs
@@ -231,8 +231,12 @@ private async Task InstallVCRedist(bool is86 = false) {
{
if (file.dest != null && file.dest.Trim() != string.Empty)
{
- var path = file.dest;
- Directory.CreateDirectory(modPath + Path.DirectorySeparatorChar + path);
+ if (!KnUtils.IsSubPath(modPath, file.dest))
+ {
+ Log.Add(Log.LogSeverity.Error, "TaskItemViewModel.InstallBuild()", $"Unsafe dest path in build '{build.id}': {file.dest}");
+ CancelTaskCommand();
+ }
+ Directory.CreateDirectory(modPath + Path.DirectorySeparatorChar + file.dest);
}
}
@@ -277,6 +281,12 @@ private async Task InstallVCRedist(bool is86 = false) {
}
Info = "Tasks: " + ProgressCurrent + "/" + ProgressBarMax;
+ if (string.IsNullOrEmpty(file.filename) || !KnUtils.IsSubPath(modPath, file.filename))
+ {
+ Log.Add(Log.LogSeverity.Error, "TaskItemViewModel.InstallBuild()", $"Unsafe filename in build '{build.id}': {file.filename}");
+ CancelTaskCommand();
+ throw new TaskCanceledException();
+ }
var fileFullPath = modPath + Path.DirectorySeparatorChar + file.filename;
var result = await fileTask.DownloadFile(file.urls!, fileFullPath, "Downloading " + file.filename, false, null, cancellationTokenSource);
diff --git a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs
index 180a35e7..3e76f791 100644
--- a/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs
+++ b/Knossos.NET/ViewModels/Templates/Tasks/InstallMod.cs
@@ -263,8 +263,13 @@ public async Task InstallMod(Mod mod, CancellationTokenSource cancelSource
{
if (file.dest != null && file.dest.Trim() != string.Empty)
{
- var path = file.dest;
- Directory.CreateDirectory(modPath + Path.DirectorySeparatorChar + path);
+ if (!KnUtils.IsSubPath(modPath, file.dest))
+ {
+ Log.Add(Log.LogSeverity.Error, "TaskItemViewModel.InstallMod()", $"Unsafe dest path in mod '{mod.id}': {file.dest}");
+ CancelTaskCommand();
+ return false;
+ }
+ Directory.CreateDirectory(modPath + Path.DirectorySeparatorChar + file.dest);
}
}
@@ -346,6 +351,12 @@ public async Task InstallMod(Mod mod, CancellationTokenSource cancelSource
{
file.dest = string.Empty;
}
+ if (string.IsNullOrEmpty(file.filename) || !KnUtils.IsSubPath(modPath, file.filename))
+ {
+ Log.Add(Log.LogSeverity.Error, "TaskItemViewModel.InstallMod()", $"Unsafe filename in mod '{mod.id}': {file.filename}");
+ CancelTaskCommand();
+ throw new TaskCanceledException();
+ }
var fileFullPath = modPath + Path.DirectorySeparatorChar + file.filename;
var result = await fileTask.DownloadFile(file.urls!, fileFullPath, "Downloading " + file.filename, false, null, cancellationTokenSource);