From 413c38dbcb7d3be15e5b7afb89a7f8b6b2e83051 Mon Sep 17 00:00:00 2001 From: dchukkapalli-dev <12871391+dchukkapalli-dev@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:59:06 +0000 Subject: [PATCH 1/2] fix(asar): prevent path traversal (zip-slip) during extraction AsarExtractor's path-traversal guard called Extensions.GetRelativePath, whose fast path strips the destination prefix literally without resolving ".." segments. A crafted archive entry such as "a/../../evil" produced a relative path that did not start with "..", so the guard passed and the file was written outside the extraction directory once the OS resolved the "..". The out-of-package symlink guard shared the same weakness. Add Extensions.IsPathInside, which normalises both paths with Path.GetFullPath before the containment check, and use it for both the file/directory and symlink guards. Co-Authored-By: Claude Opus 4.8 (1M context) --- AsarSharp/AsarExtractor.cs | 11 +++++++---- AsarSharp/Utils/Extensions.cs | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/AsarSharp/AsarExtractor.cs b/AsarSharp/AsarExtractor.cs index 6a9475e..96aca5d 100644 --- a/AsarSharp/AsarExtractor.cs +++ b/AsarSharp/AsarExtractor.cs @@ -41,9 +41,12 @@ namespace AsarSharp var destFilename = Path.Combine(dest, filename); var file = filesystem.GetFile(filename, followLinks); - // Path-traversal guard. - string relativePath = Extensions.GetRelativePath(dest, destFilename); - if (relativePath.StartsWith("..")) + // Path-traversal (zip-slip) guard. Uses the normalising + // containment check: GetRelativePath's fast path strips the + // prefix literally without resolving "..", so a crafted entry + // such as "a/../../evil" would otherwise pass this check and be + // written outside "dest". + if (!Extensions.IsPathInside(dest, destFilename)) { throw new InvalidOperationException( $"{fullPath}: file \"{destFilename}\" writes out of the package"); @@ -164,7 +167,7 @@ namespace AsarSharp var linkTo = Path.Combine(relativeLinkPath, Path.GetFileName(file.Link)); - if (Extensions.GetRelativePath(dest, linkSrcPath).StartsWith("..")) + if (!Extensions.IsPathInside(dest, linkSrcPath)) { throw new InvalidOperationException( $"{fullPath}: file \"{file.Link}\" links out of the package to \"{linkSrcPath}\""); diff --git a/AsarSharp/Utils/Extensions.cs b/AsarSharp/Utils/Extensions.cs index feeb7e0..3abaf5f 100644 --- a/AsarSharp/Utils/Extensions.cs +++ b/AsarSharp/Utils/Extensions.cs @@ -97,6 +97,27 @@ namespace AsarSharp.Utils private static bool IsSeparator(char c) => c == '/' || c == '\\'; + /// + /// Security check for archive extraction: returns true only when + /// resolves to a location inside + /// . Both paths are fully normalised first, so + /// embedded ".." segments cannot escape the root (zip-slip). The + /// fast path must not be used here because + /// it strips the prefix literally without resolving "..". + /// + public static bool IsPathInside(string root, string candidate) + { + string fullRoot = TrimTrailingSeparators(Path.GetFullPath(root)); + string fullCandidate = TrimTrailingSeparators(Path.GetFullPath(candidate)); + + if (string.Equals(fullRoot, fullCandidate, StringComparison.OrdinalIgnoreCase)) + return true; + + return fullCandidate.Length > fullRoot.Length + && fullCandidate.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase) + && IsSeparator(fullCandidate[fullRoot.Length]); + } + public static string GetDirectoryName(string path) { if (string.IsNullOrEmpty(path)) From 7d28eb7d5234ee37fe1a7f719a5a2a7eaaf5b0d5 Mon Sep 17 00:00:00 2001 From: dchukkapalli-dev <12871391+dchukkapalli-dev@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:59:06 +0000 Subject: [PATCH 2/2] fix(asar): correct Pickle payload buffer allocation Pickle.Resize allocated the backing array as _header.Length + newCapacity but advertised _capacityAfterHeader = newCapacity. On the first growth _header is still empty, so the array ended up _headerSize (4) bytes short of the header + capacity it claimed. A write that fills the payload then overran the buffer, throwing an ArgumentException when the serialised asar header was 4089-4092 bytes. Allocate _headerSize + newCapacity instead, matching Chromium's realloc(header_size_ + new_capacity). Co-Authored-By: Claude Opus 4.8 (1M context) --- AsarSharp/PickleTools/Pickle.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/AsarSharp/PickleTools/Pickle.cs b/AsarSharp/PickleTools/Pickle.cs index 169d15d..c192a8f 100644 --- a/AsarSharp/PickleTools/Pickle.cs +++ b/AsarSharp/PickleTools/Pickle.cs @@ -231,8 +231,14 @@ namespace AsarSharp.PickleTools private void Resize(int newCapacity) { newCapacity = AlignInt(newCapacity, PAYLOAD_UNIT); - byte[] newHeader = new byte[_header.Length + newCapacity]; - Buffer.BlockCopy(_header, 0, newHeader, 0, _header.Length); + // The backing array must hold the header plus the full advertised + // payload capacity (matches Chromium's realloc(header_size_ + new_capacity)). + // Sizing it from _header.Length under-allocates by _headerSize on the + // first growth (when _header is still empty), leaving the payload region + // _headerSize bytes short of _capacityAfterHeader and overrunning the + // buffer when a write fills the payload. + byte[] newHeader = new byte[_headerSize + newCapacity]; + Buffer.BlockCopy(_header, 0, newHeader, 0, Math.Min(_header.Length, newHeader.Length)); _header = newHeader; _capacityAfterHeader = newCapacity; }