diff --git a/eng/Versions.props b/eng/Versions.props index f4bf5a9ea931e7..566440a7018e2a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -229,7 +229,7 @@ 9.0.0-rtm.25627.1 9.0.0-rtm.24466.4 - 2.4.17 + 2.4.18 19.1.0-alpha.1.26202.3 19.1.0-alpha.1.26202.3 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 6e8382552e4d4b..8191f0bacd18fe 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 @@ -362,6 +362,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 symlink targets (e.g., "\Windows\win.ini"). + // Unlike files, symlink targets are resolved at access time, not extraction time, + // so Path.GetFullPath here cannot reliably predict what drive the OS will resolve them against. + 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)); @@ -591,10 +598,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 281812c11f6f21..a5a1a69dac08f6 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 @@ -135,6 +135,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)); + } } }