diff --git a/eng/Versions.props b/eng/Versions.props
index 85c36988eb8dc1..5d9f0b7c7a8920 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -233,7 +233,7 @@
8.0.0-rtm.25625.2
- 2.4.17
+ 2.4.18
16.0.5-alpha.1.25311.1
16.0.5-alpha.1.25311.1
diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs
index 83f915875d266e..4ae74325894d59 100644
--- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs
+++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs
@@ -347,6 +347,13 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b
// LinkName is an absolute path, or path relative to the fileDestinationPath directory.
// We don't check if the LinkName is empty. In that case, creation of the link will fail because link targets can't be empty.
string linkName = ArchivingUtils.SanitizeEntryFilePath(LinkName, preserveDriveRoot: true);
+ // On Windows, reject rooted but not fully qualified paths ("\Windows\win.ini", "C:foo").
+ // They resolve ambiguously based on the current drive or working directory.
+ // Symlinks are resolved at access time, so Path.GetFullPath cannot reliably determine which drive will be used.
+ if (OperatingSystem.IsWindows() && Path.IsPathRooted(linkName) && !Path.IsPathFullyQualified(linkName))
+ {
+ throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath));
+ }
string? linkDestination = GetFullDestinationPath(
destinationDirectoryPath,
Path.IsPathFullyQualified(linkName) ? linkName : Path.Join(Path.GetDirectoryName(fileDestinationPath), linkName));
@@ -576,10 +583,10 @@ private FileStreamOptions CreateFileStreamOptions(bool isAsync)
if (!OperatingSystem.IsWindows())
{
- const UnixFileMode OwnershipPermissions =
- UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
- UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute |
- UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
+ const UnixFileMode OwnershipPermissions =
+ UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
+ UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute |
+ UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute;
// Restore permissions.
// For security, limit to ownership permissions, and respect umask (through UnixCreateMode).
diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs
index 5036a59334d85b..52df8a8c52ffb7 100644
--- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs
+++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs
@@ -133,6 +133,11 @@ internal void ReplaceNormalAttributesWithExtended(Dictionary? di
// The 'size' header field only fits 12 bytes, so the data section length that surpases that limit needs to be retrieved
if (TarHelpers.TryGetStringAsBaseTenLong(ExtendedAttributes, PaxEaSize, out long size))
{
+ if (size < 0)
+ {
+ throw new InvalidDataException(SR.Format(SR.TarSizeFieldNegative));
+ }
+
_size = size;
}
diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs
index 19d5dc19627db2..a05d4c2de245a4 100644
--- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs
+++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs
@@ -27,5 +27,26 @@ public void Extract_SpecialFiles_Windows_ThrowsInvalidOperation()
Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count());
}
+
+ [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))]
+ public void ExtractToDirectory_RejectsSymlinkWithRootedTargetOutsideDestination()
+ {
+ using TempDirectory root = new TempDirectory();
+ string destDir = Path.Combine(root.Path, "dest");
+ Directory.CreateDirectory(destDir);
+
+ // A rooted path that points outside destDir (the target doesn't need to exist).
+ string rootedLinkTarget = @"\Temp\temp.ini";
+
+ string tarPath = Path.Combine(root.Path, "windows_symlink.tar");
+ using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write))
+ using (TarWriter writer = new TarWriter(stream, leaveOpen: false))
+ {
+ writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "outside.txt") { LinkName = rootedLinkTarget });
+ }
+
+ Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true));
+ Assert.Empty(Directory.EnumerateFileSystemEntries(destDir));
+ }
}
}