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