Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 89 additions & 2 deletions Microsoft.PowerShell.Archive/Microsoft.PowerShell.Archive.psm1
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

data LocalizedData
{
# culture="en-US"
Expand Down Expand Up @@ -30,6 +33,13 @@ Import-LocalizedData LocalizedData -filename ArchiveResources -ErrorAction Ignor

$zipFileExtension = ".zip"

# Reserved Windows device names used by IsValidWindowsArchiveEntryPath to guard Expand-Archive.
$script:reservedDeviceNames = New-Object -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList @(
[string[]]@('CON','PRN','AUX','NUL',
'COM1','COM2','COM3','COM4','COM5','COM6','COM7','COM8','COM9', 'COM¹', 'COM²', 'COM³',
'LPT1','LPT2','LPT3','LPT4','LPT5','LPT6','LPT7','LPT8','LPT9', 'LPT¹', 'LPT²', 'LPT³'),
[System.StringComparer]::OrdinalIgnoreCase)

<############################################################################################
# The Compress-Archive cmdlet can be used to zip/compress one or more files/directories.
############################################################################################>
Expand Down Expand Up @@ -920,6 +930,62 @@ function ValidateArchivePathHelper
}
}

<############################################################################################
# Get-WindowsArchiveEntryPathValidationResult: Validates a raw archive entry path for Windows
# and returns whether the path should be rejected plus a potentially sanitized path.
# Invalid entries include Win32 device path prefixes (\\.\ \\?\ //./ //?/) and any
# segment containing ':'. Reserved Windows device names in segments are sanitized and prefixed with '_'.
###############################################################################################>
function Get-WindowsArchiveEntryPathValidationResult
{
param([string] $Path)

$result = [PSCustomObject]@{
ContainsInvalidDevicePathPrefix = $false
IsPathModified = $false
UpdatedPath = $Path
}

if ([string]::IsNullOrEmpty($Path))
{
return $result
}

# Colons are illegal in NTFS filenames (i.e NUL:, NUL:stream, file:bad) and must be rejected.
# This method is only called with the entry name and does not contain any drive path (i.e 'C:')
# Also validate against Win32 device path prefixes (\\.\ \\?\ //./ //?/)
if ($Path -match '^(\\\\|//)[.?][/\\]' -or $Path.Contains(':'))
{
$result.ContainsInvalidDevicePathPrefix = $true
return $result
}

$updatedSegments = New-Object System.Collections.Generic.List[string]

foreach ($segment in ($Path -split '[/\\]'))
{
$updatedSegment = $segment
$trimmedSegment = $segment.TrimEnd(' .')
# Win32 strips trailing spaces and periods before resolving entries: "NUL " -> "NUL", "NUL." -> "NUL", this trimming should be preserved when validating and renaming entries.
if ($script:reservedDeviceNames.Contains($trimmedSegment))
{
$updatedSegment = '_' + $trimmedSegment
$result.IsPathModified = $true
}

$updatedSegments.Add($updatedSegment)
}

if ($result.IsPathModified)
{
$result.UpdatedPath = [string]::Join([System.IO.Path]::DirectorySeparatorChar, $updatedSegments)
$BadArchiveEntryMessage = ($LocalizedData.ReservedDeviceNameInArchiveEntry -f $Path, $result.UpdatedPath)
Write-Warning $BadArchiveEntryMessage
}

return $result
}

<############################################################################################
# ExpandArchiveHelper: This is a helper function used to expand the archive file contents
# to the specified directory.
Expand Down Expand Up @@ -990,8 +1056,29 @@ function ExpandArchiveHelper
# The archive entries can either be empty directories or files.
foreach($currentArchiveEntry in $zipArchive.Entries)
{
$entryPath = $currentArchiveEntry.FullName
$isWindowsOS = [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT

if ($isWindowsOS)
{
# Validate and sanitize the raw entry name before any path resolution.
$pathValidationResult = Get-WindowsArchiveEntryPathValidationResult -Path $entryPath

# Skip invalid paths with Win32 device path prefixes (\\.\ \\?\ //. //?)
# or segments containing ':'.
if ($pathValidationResult.ContainsInvalidDevicePathPrefix)
{
# if contains device path prefix: skip
$BadArchiveEntryMessage = ($LocalizedData.BadArchiveEntry -f $entryPath)
Write-Error $BadArchiveEntryMessage
continue
}

$entryPath = $pathValidationResult.UpdatedPath
}

# Windows filesystem provider will internally convert from `/` to `\`
$currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $currentArchiveEntry.FullName
$currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $entryPath

# Remove possible relative segments from target
# This is similar to [System.IO.Path]::GetFullPath($currentArchiveEntryPath) but uses PS current dir instead of process-wide current dir
Expand All @@ -1001,7 +1088,7 @@ function ExpandArchiveHelper
# Ordinal match is safest, case-sensitive volumes can be mounted within volumes that are case-insensitive.
if (-not ($currentArchiveEntryPath.StartsWith($expandedDir, [System.StringComparison]::Ordinal)))
{
$BadArchiveEntryMessage = ($LocalizedData.BadArchiveEntry -f $currentArchiveEntry.FullName)
$BadArchiveEntryMessage = ($LocalizedData.BadArchiveEntry -f $entryPath)
# notify user of bad archive entry
Write-Error $BadArchiveEntryMessage
# move on to the next entry in the archive
Expand Down
1 change: 1 addition & 0 deletions Microsoft.PowerShell.Archive/en-US/ArchiveResources.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ExpandProgressBarText=The archive file '{0}' expansion is in progress...
AppendArchiveFileExtensionMessage=The archive file path '{0}' supplied to the DestinationPath parameter does not include .zip extension. Hence .zip is appended to the supplied DestinationPath path and the archive file would be created at '{1}'.
AddItemtoArchiveFile=Adding '{0}'.
BadArchiveEntry=Can not process invalid archive entry '{0}'.
ReservedDeviceNameInArchiveEntry=The archive entry '{0}' contains a Windows reserved device name as one of its segments which is not supported. The entry was renamed to '{1}'.
CreateFileAtExpandedPath=Created '{0}'.
InvalidArchiveFilePathError=The archive file path '{0}' specified as input to the {1} parameter is resolving to multiple file system paths. Provide a unique path to the {2} parameter where the archive file has to be created.
InvalidExpandedDirPathError=The directory path '{0}' specified as input to the DestinationPath parameter is resolving to multiple file system paths. Provide a unique path to the Destination parameter where the archive file contents have to be expanded.
Expand Down
141 changes: 141 additions & 0 deletions Tests/Pester.Commands.Cmdlets.Archive.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,147 @@ Describe "Test suite for Microsoft.PowerShell.Archive module" -Tags "BVT" {
catch { $_.FullyQualifiedErrorId | Should Be $expectedError }
}

$testCases = @{Entry="CON"},
@{Entry="PRN"},
@{Entry="AUX"},
@{Entry="NUL"},
@{Entry="NUL "},
@{Entry="NUL."},
@{Entry="NUL.."}
@{Entry="COM1"},
@{Entry="COM2"},
@{Entry="COM3"},
@{Entry="COM4"},
@{Entry="COM5"},
@{Entry="COM6"},
@{Entry="COM7"},
@{Entry="COM8"},
@{Entry="COM9"},
@{Entry="COM¹"},
@{Entry="COM²"},
@{Entry="COM³"},
@{Entry="LPT1"},
@{Entry="LPT2"},
@{Entry="LPT3"},
@{Entry="LPT4"},
@{Entry="LPT5"},
@{Entry="LPT6"},
@{Entry="LPT7"},
@{Entry="LPT8"},
@{Entry="LPT9"},
@{Entry="LPT¹"},
@{Entry="LPT²"},
@{Entry="LPT³"}

It "Validate Expand-Archive renames entries using reserved Windows device name '<Entry>'" -TestCases $testCases -skip:(!$IsWindows) {
param($Entry)
$archivePath = "$TestDrive$($DS)ReservedDeviceNameEntry.zip"
$destinationPath = "$TestDrive$($DS)ReservedDeviceNameEntry"

Add-CompressionAssemblies

$archiveFileStreamArgs = @($archivePath, [System.IO.FileMode]::Create)
$archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs

$zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Create, $false)
$zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs

$entry = $zipArchive.CreateEntry($Entry)
$entryStream = $entry.Open()
$entryWriter = New-Object -TypeName System.IO.StreamWriter -ArgumentList $entryStream
$entryWriter.Write("Invalid Entry Content")
$entryWriter.Dispose()

if ($zipArchive) { $zipArchive.Dispose() }
if ($archiveFileStream) { $archiveFileStream.Dispose() }

try {
Expand-Archive -Path $archivePath -DestinationPath $destinationPath -WarningVariable WarningVar -WarningAction SilentlyContinue

$renamedFileName = "_$($Entry.ToString().TrimEnd(' .'))"
$renamedFilePath = Join-Path $destinationPath -ChildPath $renamedFileName
$renamedFileExists = Test-Path $renamedFilePath
$renamedFileExists | Should Be $true

$WarningVar.Count | Should -Be 1
$WarningVar[0] | Should Match "Windows reserved device name as one of its segments which is not supported. The entry was renamed"
}
finally
{
Remove-Item -LiteralPath "$TestDrive$($DS)ReservedDeviceNameEntry" -Force -Recurse
}

}

$fileNameWithRerservedDeviceNameStemTestCases = @{Entry="NUL.tar.gz"},
@{Entry="NUL.txt"}

It "Validate Expand-Archive allows entries that have a Windows reserved device name stem but also include file extension '<Entry>'" -TestCases $fileNameWithRerservedDeviceNameStemTestCases -skip:(!$IsWindows) {
param($Entry)
$archivePath = "$TestDrive$($DS)ReservedDeviceNameEntry.zip"
$destinationPath = "$TestDrive$($DS)ReservedDeviceNameEntry"

Add-CompressionAssemblies

$archiveFileStreamArgs = @($archivePath, [System.IO.FileMode]::Create)
$archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs

$zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Create, $false)
$zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs

$entry = $zipArchive.CreateEntry($Entry)
$entryStream = $entry.Open()
$entryWriter = New-Object -TypeName System.IO.StreamWriter -ArgumentList $entryStream
$entryWriter.Write("Valid Entry Content")
$entryWriter.Dispose()

if ($zipArchive) { $zipArchive.Dispose() }
if ($archiveFileStream) { $archiveFileStream.Dispose() }

Expand-Archive -Path $archivePath -DestinationPath $destinationPath -WarningVariable WarningVar -WarningAction SilentlyContinue
$archiveEntryFilePath = Join-Path $destinationPath -ChildPath "$Entry"
$archiveEntryFileExists = Test-Path $archiveEntryFilePath
$archiveEntryFileExists | Should Be $true

$WarningVar.Count | Should -Be 0
}

$win32DevicePathPrefixTestCases = @(
@{ Entry = "\\.\file1.txt" },
@{ Entry = "\\?\$TestDrive$($DS)file1.txt" },
@{ Entry = "//./file1.txt" },
@{ Entry = "//?/$TestDrive$($DS)file1.txt" }
)

It "Validate Expand-Archive rejects zip entries using Win32 device path prefix <Entry>" -TestCases $win32DevicePathPrefixTestCases -skip:(!$IsWindows) {
param($Entry)
$archivePath = "$TestDrive$($DS)Win32DevicePathPrefixEntry.zip"
$destinationPath = "$TestDrive$($DS)Win32DevicePathPrefixEntry"

Add-CompressionAssemblies

$archiveFileStreamArgs = @($archivePath, [System.IO.FileMode]::Create)
$archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs

$zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Create, $false)
$zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs

$entry = $zipArchive.CreateEntry($Entry)
$entryStream = $entry.Open()
$entryWriter = New-Object -TypeName System.IO.StreamWriter -ArgumentList $entryStream
$entryWriter.Write("Invalid Entry Content")
$entryWriter.Dispose()

if ($zipArchive) { $zipArchive.Dispose() }
if ($archiveFileStream) { $archiveFileStream.Dispose() }

$res = Test-Path $archivePath
$res | Should Be $true
Expand-Archive -Path $archivePath -DestinationPath $destinationPath -ErrorAction SilentlyContinue -ErrorVariable err
$err.Count | Should -Be 1
$err[0].Exception.Message | Should Match "invalid archive entry"
}

It "Validate that you can compress an archive to a custom PSDrive using the Compress-Archive cmdlet" {
$sourcePath = "$TestDrive$($DS)SourceDir$($DS)ChildDir-1$($DS)Sample-3.txt"
$destinationDriveName = 'CompressArchivePesterTest'
Expand Down
Loading