From 1cde521d75de5707ccb961884613582d12e8215a Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:52:28 +0800 Subject: [PATCH] Prevent zip path traversal in app upgrade Hardened upgrade extraction by validating each zip entry resolves under the app startup directory before writing files, and aborting with a clear failure message on suspicious paths. Added shared path utilities for directory containment checks and OS-aware path string comparison, then reused them for executable matching and `bin` prefix checks during extraction. https://github.com/2dust/v2rayN/issues/10313 --- v2rayN/AmazTool/UpgradeApp.cs | 13 ++++++++++--- v2rayN/AmazTool/Utils.cs | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/v2rayN/AmazTool/UpgradeApp.cs b/v2rayN/AmazTool/UpgradeApp.cs index caa269f4..f2ffe522 100644 --- a/v2rayN/AmazTool/UpgradeApp.cs +++ b/v2rayN/AmazTool/UpgradeApp.cs @@ -45,6 +45,8 @@ internal class UpgradeApp var thisAppOldFile = $"{Utils.GetExePath()}.tmp"; File.Delete(thisAppOldFile); var splitKey = "/"; + var allowedBaseDir = Utils.StartupPath(); + var pathComparison = Utils.GetPathComparison(); using var archive = ZipFile.OpenRead(fileName); foreach (var entry in archive.Entries) @@ -65,16 +67,21 @@ internal class UpgradeApp } var fullName = string.Join(splitKey, lst[1..lst.Length]); + var entryOutputPath = Utils.GetPath(fullName); + if (!Utils.IsPathUnderDirectory(allowedBaseDir, entryOutputPath)) + { + Console.WriteLine($"{Resx.Resource.FailedUpgrade} blocked potential path traversal: {entry.FullName}"); + return; + } - if (string.Equals(Utils.GetExePath(), Utils.GetPath(fullName), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(Utils.GetExePath(), entryOutputPath, pathComparison)) { File.Move(Utils.GetExePath(), thisAppOldFile); } - var entryOutputPath = Utils.GetPath(fullName); Directory.CreateDirectory(Path.GetDirectoryName(entryOutputPath)!); //In the bin folder, if the file already exists, it will be skipped - if (fullName.StartsWith("bin") && File.Exists(entryOutputPath)) + if (fullName.StartsWith("bin", pathComparison) && File.Exists(entryOutputPath)) { continue; } diff --git a/v2rayN/AmazTool/Utils.cs b/v2rayN/AmazTool/Utils.cs index df13ecf6..b7adc346 100644 --- a/v2rayN/AmazTool/Utils.cs +++ b/v2rayN/AmazTool/Utils.cs @@ -24,6 +24,28 @@ internal class Utils return Path.Combine(startupPath, fileName); } + public static bool IsPathUnderDirectory(string baseDir, string targetPath) + { + if (string.IsNullOrWhiteSpace(baseDir) || string.IsNullOrWhiteSpace(targetPath)) + { + return false; + } + + var baseFull = Path.GetFullPath(baseDir); + var targetFull = Path.GetFullPath(targetPath); + + baseFull = baseFull.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + + return targetFull.StartsWith(baseFull, GetPathComparison()); + } + + public static StringComparison GetPathComparison() + { + return OperatingSystem.IsWindows() + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + } + public static string V2rayN => "v2rayN"; public static void StartV2RayN()