mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-22 06:54:40 +00:00
fixes SharpCompress TAR extraction overwrites files outside the extraction root when a SymbolicLinkHandler enables symlink chaining
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
@@ -8,6 +9,10 @@ internal static class DirectoryManagement
|
||||
"Entry is trying to create a directory outside of the destination directory.";
|
||||
internal const string WriteFileOutsideDestinationMessage =
|
||||
"Entry is trying to write a file outside of the destination directory.";
|
||||
internal const string LinkTargetOutsideDestinationMessage =
|
||||
"Entry is trying to create a symbolic link outside of the destination directory.";
|
||||
internal const string ReparsePointInDestinationMessage =
|
||||
"Entry is trying to extract through a symbolic link or reparse point.";
|
||||
|
||||
internal static string GetFullDestinationDirectoryPath(string destinationDirectory)
|
||||
{
|
||||
@@ -58,6 +63,114 @@ internal static class DirectoryManagement
|
||||
throw new ExtractionException(exceptionMessage);
|
||||
}
|
||||
|
||||
internal static void EnsureNoReparsePointInDestinationDirectory(
|
||||
string destinationPath,
|
||||
string fullDestinationDirectoryPath
|
||||
)
|
||||
{
|
||||
var destinationDirectoryPath = TrimTrailingDirectorySeparators(
|
||||
fullDestinationDirectoryPath
|
||||
);
|
||||
EnsurePathIsNotReparsePoint(destinationDirectoryPath);
|
||||
|
||||
if (string.Equals(destinationPath, destinationDirectoryPath, Utility.PathComparison))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var relativeDestinationPath = destinationPath.Substring(
|
||||
fullDestinationDirectoryPath.Length
|
||||
);
|
||||
var path = destinationDirectoryPath;
|
||||
|
||||
foreach (
|
||||
var pathPart in relativeDestinationPath.Split(
|
||||
new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
|
||||
StringSplitOptions.RemoveEmptyEntries
|
||||
)
|
||||
)
|
||||
{
|
||||
path = Path.Combine(path, pathPart);
|
||||
|
||||
if (!PathExistsAndIsNotReparsePoint(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void CreateDirectory(
|
||||
string destinationPath,
|
||||
string fullDestinationDirectoryPath
|
||||
)
|
||||
{
|
||||
EnsureNoReparsePointInDestinationDirectory(destinationPath, fullDestinationDirectoryPath);
|
||||
|
||||
if (!Directory.Exists(destinationPath))
|
||||
{
|
||||
Directory.CreateDirectory(destinationPath);
|
||||
}
|
||||
|
||||
EnsureNoReparsePointInDestinationDirectory(destinationPath, fullDestinationDirectoryPath);
|
||||
}
|
||||
|
||||
internal static void EnsureLinkTargetInDestinationDirectory(
|
||||
string destinationFileName,
|
||||
string linkTarget,
|
||||
string fullDestinationDirectoryPath
|
||||
)
|
||||
{
|
||||
var destinationDirectory = Path.GetDirectoryName(destinationFileName)
|
||||
.NotNull("Destination directory is null");
|
||||
var fullLinkTargetPath = Path.GetFullPath(Path.Combine(destinationDirectory, linkTarget));
|
||||
|
||||
EnsurePathInDestinationDirectory(
|
||||
fullLinkTargetPath,
|
||||
fullDestinationDirectoryPath,
|
||||
LinkTargetOutsideDestinationMessage
|
||||
);
|
||||
}
|
||||
|
||||
internal static void EnsurePathIsNotReparsePoint(string path)
|
||||
{
|
||||
PathExistsAndIsNotReparsePoint(path);
|
||||
}
|
||||
|
||||
private static bool PathExistsAndIsNotReparsePoint(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if ((File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0)
|
||||
{
|
||||
throw new ExtractionException(ReparsePointInDestinationMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException exception)
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Unable to verify the extraction path for symbolic links or reparse points.",
|
||||
exception
|
||||
);
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Unable to verify the extraction path for symbolic links or reparse points.",
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDirectorySeparator(char value) =>
|
||||
value == Path.DirectorySeparatorChar || value == Path.AltDirectorySeparatorChar;
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ public sealed record ExtractionOptions : IExtractionOptions
|
||||
/// <remarks>
|
||||
/// <b>Breaking change:</b> Changed from field to property in version 0.40.0.
|
||||
/// If no handler is provided, symbolic links are silently skipped during extraction.
|
||||
/// Directory extraction rejects link targets outside the destination directory and does not
|
||||
/// follow symbolic links or reparse points in later entry paths.
|
||||
/// </remarks>
|
||||
public Action<string, string>? SymbolicLinkHandler { get; set; }
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ internal static partial class IEntryExtensions
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (entry.LinkTarget is not null && options.SymbolicLinkHandler is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var destinationFileName = GetEntryDestinationFileName(
|
||||
entry,
|
||||
fullDestinationDirectoryPath,
|
||||
@@ -54,6 +59,20 @@ internal static partial class IEntryExtensions
|
||||
DirectoryManagement.WriteFileOutsideDestinationMessage
|
||||
);
|
||||
|
||||
DirectoryManagement.EnsureNoReparsePointInDestinationDirectory(
|
||||
destinationFileName,
|
||||
fullDestinationDirectoryPath
|
||||
);
|
||||
|
||||
if (entry.LinkTarget is not null)
|
||||
{
|
||||
DirectoryManagement.EnsureLinkTargetInDestinationDirectory(
|
||||
destinationFileName,
|
||||
entry.LinkTarget,
|
||||
fullDestinationDirectoryPath
|
||||
);
|
||||
}
|
||||
|
||||
if (writeAsync != null)
|
||||
{
|
||||
await writeAsync(destinationFileName, cancellationToken).ConfigureAwait(false);
|
||||
@@ -69,10 +88,10 @@ internal static partial class IEntryExtensions
|
||||
DirectoryManagement.CreateDirectoryOutsideDestinationMessage
|
||||
);
|
||||
|
||||
if (!Directory.Exists(destinationFileName))
|
||||
{
|
||||
Directory.CreateDirectory(destinationFileName);
|
||||
}
|
||||
DirectoryManagement.CreateDirectory(
|
||||
destinationFileName,
|
||||
fullDestinationDirectoryPath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +109,8 @@ internal static partial class IEntryExtensions
|
||||
}
|
||||
else
|
||||
{
|
||||
DirectoryManagement.EnsurePathIsNotReparsePoint(destinationFileName);
|
||||
|
||||
var fm = FileMode.Create;
|
||||
|
||||
if (!options.Overwrite)
|
||||
|
||||
@@ -45,6 +45,11 @@ internal static partial class IEntryExtensions
|
||||
Action<string>? write
|
||||
)
|
||||
{
|
||||
if (entry.LinkTarget is not null && options.SymbolicLinkHandler is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var destinationFileName = GetEntryDestinationFileName(
|
||||
entry,
|
||||
fullDestinationDirectoryPath,
|
||||
@@ -60,6 +65,21 @@ internal static partial class IEntryExtensions
|
||||
fullDestinationDirectoryPath,
|
||||
DirectoryManagement.WriteFileOutsideDestinationMessage
|
||||
);
|
||||
|
||||
DirectoryManagement.EnsureNoReparsePointInDestinationDirectory(
|
||||
destinationFileName,
|
||||
fullDestinationDirectoryPath
|
||||
);
|
||||
|
||||
if (entry.LinkTarget is not null)
|
||||
{
|
||||
DirectoryManagement.EnsureLinkTargetInDestinationDirectory(
|
||||
destinationFileName,
|
||||
entry.LinkTarget,
|
||||
fullDestinationDirectoryPath
|
||||
);
|
||||
}
|
||||
|
||||
write?.Invoke(destinationFileName);
|
||||
}
|
||||
else if (options.ExtractFullPath)
|
||||
@@ -72,10 +92,10 @@ internal static partial class IEntryExtensions
|
||||
DirectoryManagement.CreateDirectoryOutsideDestinationMessage
|
||||
);
|
||||
|
||||
if (!Directory.Exists(destinationFileName))
|
||||
{
|
||||
Directory.CreateDirectory(destinationFileName);
|
||||
}
|
||||
DirectoryManagement.CreateDirectory(
|
||||
destinationFileName,
|
||||
fullDestinationDirectoryPath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,10 +122,7 @@ internal static partial class IEntryExtensions
|
||||
: DirectoryManagement.WriteFileOutsideDestinationMessage
|
||||
);
|
||||
|
||||
if (!Directory.Exists(destdir))
|
||||
{
|
||||
Directory.CreateDirectory(destdir);
|
||||
}
|
||||
DirectoryManagement.CreateDirectory(destdir, fullDestinationDirectoryPath);
|
||||
|
||||
return Path.Combine(destdir, file);
|
||||
}
|
||||
@@ -126,6 +143,8 @@ internal static partial class IEntryExtensions
|
||||
}
|
||||
else
|
||||
{
|
||||
DirectoryManagement.EnsurePathIsNotReparsePoint(destinationFileName);
|
||||
|
||||
var fm = FileMode.Create;
|
||||
|
||||
if (!options.Overwrite)
|
||||
|
||||
@@ -39,6 +39,8 @@ public interface IExtractionOptions
|
||||
/// Delegate for writing symbolic links to disk.
|
||||
/// The first parameter is the source path (where the symlink is created).
|
||||
/// The second parameter is the target path (what the symlink refers to).
|
||||
/// Directory extraction rejects link targets outside the destination directory and does not
|
||||
/// follow symbolic links or reparse points in later entry paths.
|
||||
/// </summary>
|
||||
Action<string, string>? SymbolicLinkHandler { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user