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);