From f89dd9548039d1f2fdfc3a38816c4ce575d3b938 Mon Sep 17 00:00:00 2001 From: LastBattle <4586194+lastbattle@users.noreply.github.com> Date: Mon, 18 May 2026 00:10:24 +0900 Subject: [PATCH 1/3] Fix IMG path traversal validation Canonicalize IMG file paths before filesystem access and reject paths that escape the configured version directory. --- MapleLib/Img/ImgFileSystemManager.cs | 61 ++++++++++++++++------------ 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/MapleLib/Img/ImgFileSystemManager.cs b/MapleLib/Img/ImgFileSystemManager.cs index 5f0dce8..6f5acfb 100644 --- a/MapleLib/Img/ImgFileSystemManager.cs +++ b/MapleLib/Img/ImgFileSystemManager.cs @@ -111,7 +111,7 @@ public ImgFileSystemManager( if (!Directory.Exists(versionPath)) throw new DirectoryNotFoundException($"Version directory not found: {versionPath}"); - _versionPath = versionPath; + _versionPath = Path.GetFullPath(versionPath); _config = config ?? new HaCreatorConfig(); // Initialize LRU cache with size-based eviction @@ -122,12 +122,12 @@ public ImgFileSystemManager( _imageCache = new LRUCache(maxCacheBytes, EstimateWzImageSize); // Load version manifest - string manifestPath = Path.Combine(versionPath, MANIFEST_FILENAME); + string manifestPath = Path.Combine(_versionPath, MANIFEST_FILENAME); if (File.Exists(manifestPath)) { string json = File.ReadAllText(manifestPath); _versionInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(json); - _versionInfo.DirectoryPath = versionPath; + _versionInfo.DirectoryPath = _versionPath; } else { @@ -136,7 +136,7 @@ public ImgFileSystemManager( { Version = Path.GetFileName(versionPath), DisplayName = Path.GetFileName(versionPath), - DirectoryPath = versionPath, + DirectoryPath = _versionPath, ExtractedDate = DateTime.Now }; } @@ -269,9 +269,7 @@ public WzImage LoadImage(string category, string relativePath) Interlocked.Increment(ref _cacheMisses); // Build full path - string fullPath = Path.Combine(_versionPath, category, relativePath); - if (!fullPath.EndsWith(".img", StringComparison.OrdinalIgnoreCase)) - fullPath += ".img"; + string fullPath = GetContainedImagePath(category, relativePath); if (!File.Exists(fullPath)) return null; @@ -489,10 +487,7 @@ public bool CategoryExists(string category) /// public bool ImageExists(string category, string relativePath) { - string fullPath = Path.Combine(_versionPath, category, relativePath); - if (!fullPath.EndsWith(".img", StringComparison.OrdinalIgnoreCase)) - fullPath += ".img"; - return File.Exists(fullPath); + return File.Exists(GetContainedImagePath(category, relativePath)); } /// @@ -500,9 +495,7 @@ public bool ImageExists(string category, string relativePath) /// public string GetImageDiagnostics(string category, string relativePath) { - string fullPath = Path.Combine(_versionPath, category, relativePath); - if (!fullPath.EndsWith(".img", StringComparison.OrdinalIgnoreCase)) - fullPath += ".img"; + string fullPath = GetContainedImagePath(category, relativePath); string categoryPath = Path.Combine(_versionPath, category); bool categoryDirExists = Directory.Exists(categoryPath); @@ -557,9 +550,7 @@ public bool SaveImage(WzImage image, string category, string relativePath) if (image == null) throw new ArgumentNullException(nameof(image)); - string fullPath = Path.Combine(_versionPath, category, relativePath); - if (!fullPath.EndsWith(".img", StringComparison.OrdinalIgnoreCase)) - fullPath += ".img"; + string fullPath = GetContainedImagePath(category, relativePath); return SaveImageToFile(image, fullPath); } @@ -619,6 +610,29 @@ public bool SaveImageToFile(WzImage image, string filePath) } } + private string GetContainedImagePath(string category, string relativePath) + { + if (string.IsNullOrWhiteSpace(category)) + throw new ArgumentException("Category is required.", nameof(category)); + if (string.IsNullOrWhiteSpace(relativePath)) + throw new ArgumentException("Relative path is required.", nameof(relativePath)); + + string fullPath = Path.Combine(_versionPath, category, relativePath); + if (!fullPath.EndsWith(".img", StringComparison.OrdinalIgnoreCase)) + fullPath += ".img"; + + fullPath = Path.GetFullPath(fullPath); + string root = Path.GetFullPath(_versionPath); + string rootWithSeparator = Path.EndsInDirectorySeparator(root) + ? root + : root + Path.DirectorySeparatorChar; + + if (!fullPath.StartsWith(rootWithSeparator, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException($"Image path escapes the version directory: {fullPath}"); + + return fullPath; + } + /// /// Gets the cache key from a full file path /// @@ -644,9 +658,7 @@ private string GetCacheKeyFromPath(string filePath) /// The created WzImage or null if failed public WzImage CreateImage(string category, string relativePath) { - string fullPath = Path.Combine(_versionPath, category, relativePath); - if (!fullPath.EndsWith(".img", StringComparison.OrdinalIgnoreCase)) - fullPath += ".img"; + string fullPath = GetContainedImagePath(category, relativePath); // Check if file already exists if (File.Exists(fullPath)) @@ -689,9 +701,7 @@ public WzImage CreateImage(string category, string relativePath) /// True if deleted successfully public bool DeleteImage(string category, string relativePath) { - string fullPath = Path.Combine(_versionPath, category, relativePath); - if (!fullPath.EndsWith(".img", StringComparison.OrdinalIgnoreCase)) - fullPath += ".img"; + string fullPath = GetContainedImagePath(category, relativePath); try { @@ -724,10 +734,7 @@ public bool DeleteImage(string category, string relativePath) /// public string GetImagePath(string category, string relativePath) { - string fullPath = Path.Combine(_versionPath, category, relativePath); - if (!fullPath.EndsWith(".img", StringComparison.OrdinalIgnoreCase)) - fullPath += ".img"; - return fullPath; + return GetContainedImagePath(category, relativePath); } #endregion From 37ddb49c2655b8bf9cefffdbbb032581c5600266 Mon Sep 17 00:00:00 2001 From: LastBattle <4586194+lastbattle@users.noreply.github.com> Date: Mon, 18 May 2026 00:14:56 +0900 Subject: [PATCH 2/3] Handle empty WZ image paths Return null for empty or separator-only WzImage paths instead of indexing an empty segment list. --- MapleLib/WzLib/WzImage.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MapleLib/WzLib/WzImage.cs b/MapleLib/WzLib/WzImage.cs index 5dc4afb..8a3bcf5 100644 --- a/MapleLib/WzLib/WzImage.cs +++ b/MapleLib/WzLib/WzImage.cs @@ -247,8 +247,14 @@ public WzImageProperty GetFromPath(string path) { if (reader != null) if (!parsed) ParseImage(); + if (string.IsNullOrWhiteSpace(path)) + return null; + string[] segments = path.Split(new char[1] { '/' }, System.StringSplitOptions.RemoveEmptyEntries); + if (segments.Length == 0) + return null; + // If the first segment is "..", return null if (segments[0] == "..") return null; From d0e6a18a36db21376b9c11ca0ce9fdbfa8360d17 Mon Sep 17 00:00:00 2001 From: LastBattle <4586194+lastbattle@users.noreply.github.com> Date: Mon, 18 May 2026 00:17:10 +0900 Subject: [PATCH 3/3] Require exact fixed-length string reads Use ReadExactly for WzBinaryReader fixed-length string data so truncated streams fail instead of decoding partially filled buffers. --- MapleLib/WzLib/Util/WzBinaryReader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MapleLib/WzLib/Util/WzBinaryReader.cs b/MapleLib/WzLib/Util/WzBinaryReader.cs index 10d8c1b..01a08ad 100644 --- a/MapleLib/WzLib/Util/WzBinaryReader.cs +++ b/MapleLib/WzLib/Util/WzBinaryReader.cs @@ -191,7 +191,7 @@ public string ReadString(int length) ? stackalloc byte[length] : (pooledArray = s_bytePool.Rent(length)).AsSpan(0, length); - BaseStream.Read(buffer); + BaseStream.ReadExactly(buffer); return Encoding.ASCII.GetString(buffer); } finally @@ -444,4 +444,4 @@ public override void Close() } #endregion } -} \ No newline at end of file +}