mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-08-28 17:01:04 +00:00
413c38dbcb
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) <noreply@anthropic.com>
222 lines
8.1 KiB
C#
222 lines
8.1 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace AsarSharp.Utils
|
|
{
|
|
internal static class Extensions
|
|
{
|
|
/// <summary>
|
|
/// Compute path relative to <paramref name="relativeTo"/>.
|
|
/// Fast common-case (path is inside relativeTo): plain prefix-strip.
|
|
/// Falls back to <see cref="Path.GetFullPath"/> + manual relativisation
|
|
/// when paths must be normalised or '..' segments are required.
|
|
/// Replaces previous URI-based implementation which was a large hot-path cost.
|
|
/// </summary>
|
|
public static string GetRelativePath(string relativeTo, string path)
|
|
{
|
|
if (string.IsNullOrEmpty(relativeTo))
|
|
throw new ArgumentNullException(nameof(relativeTo));
|
|
if (string.IsNullOrEmpty(path))
|
|
throw new ArgumentNullException(nameof(path));
|
|
|
|
// Fast path: literal prefix match (no normalisation). Covers ~all
|
|
// intra-archive callers where both inputs already come from the
|
|
// same crawl pass.
|
|
string baseFast = TrimTrailingSeparators(relativeTo);
|
|
string pathFast = TrimTrailingSeparators(path);
|
|
|
|
if (string.Equals(baseFast, pathFast, StringComparison.OrdinalIgnoreCase))
|
|
return string.Empty;
|
|
|
|
if (pathFast.Length > baseFast.Length &&
|
|
pathFast.StartsWith(baseFast, StringComparison.OrdinalIgnoreCase) &&
|
|
IsSeparator(pathFast[baseFast.Length]))
|
|
{
|
|
return pathFast.Substring(baseFast.Length + 1);
|
|
}
|
|
|
|
// Slow path: normalise both sides and compute relative — used for
|
|
// security checks (out-of-tree symlink/destination guards) and the
|
|
// rare "go up" case.
|
|
return GetRelativePathNormalised(relativeTo, path);
|
|
}
|
|
|
|
private static string GetRelativePathNormalised(string relativeTo, string path)
|
|
{
|
|
string fullBase = Path.GetFullPath(relativeTo);
|
|
string fullPath = Path.GetFullPath(path);
|
|
|
|
fullBase = TrimTrailingSeparators(fullBase);
|
|
fullPath = TrimTrailingSeparators(fullPath);
|
|
|
|
if (string.Equals(fullBase, fullPath, StringComparison.OrdinalIgnoreCase))
|
|
return string.Empty;
|
|
|
|
if (fullPath.Length > fullBase.Length &&
|
|
fullPath.StartsWith(fullBase, StringComparison.OrdinalIgnoreCase) &&
|
|
IsSeparator(fullPath[fullBase.Length]))
|
|
{
|
|
return fullPath.Substring(fullBase.Length + 1);
|
|
}
|
|
|
|
// Need to walk up the common ancestor.
|
|
char sep = Path.DirectorySeparatorChar;
|
|
string[] baseParts = fullBase.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
|
|
string[] pathParts = fullPath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
int common = 0;
|
|
int max = Math.Min(baseParts.Length, pathParts.Length);
|
|
while (common < max &&
|
|
string.Equals(baseParts[common], pathParts[common], StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
common++;
|
|
}
|
|
|
|
var sb = new StringBuilder();
|
|
for (int i = common; i < baseParts.Length; i++)
|
|
{
|
|
if (sb.Length > 0) sb.Append(sep);
|
|
sb.Append("..");
|
|
}
|
|
for (int i = common; i < pathParts.Length; i++)
|
|
{
|
|
if (sb.Length > 0) sb.Append(sep);
|
|
sb.Append(pathParts[i]);
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string TrimTrailingSeparators(string s)
|
|
{
|
|
int end = s.Length;
|
|
while (end > 0 && IsSeparator(s[end - 1])) end--;
|
|
return end == s.Length ? s : s.Substring(0, end);
|
|
}
|
|
|
|
private static bool IsSeparator(char c) => c == '/' || c == '\\';
|
|
|
|
/// <summary>
|
|
/// Security check for archive extraction: returns true only when
|
|
/// <paramref name="candidate"/> resolves to a location inside
|
|
/// <paramref name="root"/>. Both paths are fully normalised first, so
|
|
/// embedded ".." segments cannot escape the root (zip-slip). The
|
|
/// <see cref="GetRelativePath"/> fast path must not be used here because
|
|
/// it strips the prefix literally without resolving "..".
|
|
/// </summary>
|
|
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))
|
|
return ".";
|
|
|
|
string result = Path.GetDirectoryName(path);
|
|
|
|
if (string.IsNullOrEmpty(result))
|
|
return ".";
|
|
|
|
return result;
|
|
}
|
|
|
|
public static void CopyDirectory(string sourceDir, string destinationDir)
|
|
{
|
|
Directory.CreateDirectory(destinationDir);
|
|
|
|
foreach (var file in Directory.GetFiles(sourceDir))
|
|
{
|
|
var destFile = Path.Combine(destinationDir, Path.GetFileName(file));
|
|
File.Copy(file, destFile, true);
|
|
}
|
|
|
|
foreach (var dir in Directory.GetDirectories(sourceDir))
|
|
{
|
|
var destDir = Path.Combine(destinationDir, Path.GetFileName(dir));
|
|
CopyDirectory(dir, destDir);
|
|
}
|
|
}
|
|
|
|
public static string GetBasePath(string dir)
|
|
{
|
|
int wildcardIndex = dir.IndexOfAny(new[] { '*', '?' });
|
|
if (wildcardIndex == -1)
|
|
{
|
|
return dir;
|
|
}
|
|
|
|
int lastSeparatorIndex = dir.LastIndexOf(Path.DirectorySeparatorChar, wildcardIndex);
|
|
if (lastSeparatorIndex == -1)
|
|
{
|
|
return ".";
|
|
}
|
|
|
|
return dir.Substring(0, lastSeparatorIndex);
|
|
}
|
|
|
|
public static void SetUnixFilePermission(string filePath, string permission)
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
return;
|
|
|
|
var process = new System.Diagnostics.Process
|
|
{
|
|
StartInfo = new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = "chmod",
|
|
Arguments = $"{permission} \"{filePath}\"",
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
CreateNoWindow = true
|
|
}
|
|
};
|
|
process.Start();
|
|
process.WaitForExit();
|
|
}
|
|
|
|
|
|
public static void CreateSymbolicLink(string linkTarget, string linkPath)
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
NativeMethods.CreateSymbolicLink(linkPath, linkTarget,
|
|
Directory.Exists(linkTarget)
|
|
? NativeMethods.SymLinkFlag.Directory
|
|
: NativeMethods.SymLinkFlag.File);
|
|
return;
|
|
}
|
|
|
|
var process = new System.Diagnostics.Process
|
|
{
|
|
StartInfo = new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = "ln",
|
|
Arguments = $"-s \"{linkTarget}\" \"{linkPath}\"",
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
CreateNoWindow = true
|
|
}
|
|
};
|
|
process.Start();
|
|
process.WaitForExit();
|
|
}
|
|
|
|
|
|
public static bool IsWindowsPlatform()
|
|
{
|
|
return Environment.OSVersion.Platform == PlatformID.Win32NT;
|
|
}
|
|
}
|
|
}
|