Harden Windows release mode harness
This commit is contained in:
1 parent
8748760594
commit
81ab7aac39
2 files changed
+65
-11
No files matched your search
@@ -4,15 +4,28 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repository = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
$installerPath = Join-Path $PSScriptRoot 'install_windows_release_client.ps1'
|
||||
$fixtureScript = Join-Path $PSScriptRoot 'test_windows_release_launcher.py'
|
||||
$temporaryRoot = Join-Path ([IO.Path]::GetTempPath()) ('akyldash-bundle-mode-' + [guid]::NewGuid().ToString('N'))
|
||||
$temporaryBase = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
|
||||
$temporaryBase = [IO.Path]::GetFullPath((Resolve-Path -LiteralPath $temporaryBase).Path).TrimEnd('\')
|
||||
$temporaryName = 'akyldash-bundle-mode-' + [guid]::NewGuid().ToString('N')
|
||||
$temporaryRoot = [IO.Path]::Combine($temporaryBase, $temporaryName)
|
||||
if (Test-Path -LiteralPath $temporaryRoot) { throw 'Temporary fixture directory already exists.' }
|
||||
[void][IO.Directory]::CreateDirectory($temporaryRoot)
|
||||
$temporaryRootItem = Get-Item -LiteralPath $temporaryRoot -Force
|
||||
if (($temporaryRootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
||||
throw 'Temporary fixture root must not be a reparse point.'
|
||||
}
|
||||
$temporaryRootCreated = $temporaryRootItem.CreationTimeUtc
|
||||
$temporaryAclOwner = (Get-Acl -LiteralPath $temporaryRoot).Owner
|
||||
$temporaryOwnerSid = ([Security.Principal.NTAccount]$temporaryAclOwner).Translate(
|
||||
[Security.Principal.SecurityIdentifier]).Value
|
||||
$currentSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
||||
if ($temporaryOwnerSid -cne $currentSid) { throw 'Temporary fixture root is not owned by the current user.' }
|
||||
$bundlePath = Join-Path $temporaryRoot 'fixture.zip'
|
||||
|
||||
try {
|
||||
$fixtureOutput = & $PythonPath -E -s -S $fixtureScript --write-bundle-fixture $bundlePath
|
||||
$fixtureOutput = & $PythonPath -B -E -s -S $fixtureScript --write-bundle-fixture $bundlePath
|
||||
if ($LASTEXITCODE -ne 0) { throw 'The temporary packager fixture failed.' }
|
||||
$fixture = ($fixtureOutput -join "`n") | ConvertFrom-Json
|
||||
|
||||
@@ -55,13 +68,14 @@ try {
|
||||
$copy = $targetArchive.CreateEntry($entry.FullName, [System.IO.Compression.CompressionLevel]::NoCompression)
|
||||
$copy.ExternalAttributes = $entry.ExternalAttributes
|
||||
if (-not $changed) {
|
||||
$copy.ExternalAttributes = [int]([long]0x81A0 -shl 16)
|
||||
$copy.ExternalAttributes = [BitConverter]::ToInt32(
|
||||
[BitConverter]::GetBytes([uint32]0x81A00000), 0)
|
||||
$changed = $true
|
||||
}
|
||||
$input = $entry.Open()
|
||||
$output = $copy.Open()
|
||||
try { $input.CopyTo($output) }
|
||||
finally { $output.Dispose(); $input.Dispose() }
|
||||
$entryInput = $entry.Open()
|
||||
$entryOutput = $copy.Open()
|
||||
try { $entryInput.CopyTo($entryOutput) }
|
||||
finally { $entryOutput.Dispose(); $entryInput.Dispose() }
|
||||
}
|
||||
if (-not $changed) { throw 'Packager fixture had no entries to mutate.' }
|
||||
} finally { $targetArchive.Dispose(); $sourceArchive.Dispose() }
|
||||
@@ -72,14 +86,45 @@ try {
|
||||
$alteredArchive = [System.IO.Compression.ZipArchive]::new(
|
||||
$alteredStream, [System.IO.Compression.ZipArchiveMode]::Read, $true)
|
||||
try {
|
||||
$alteredEntry = $alteredArchive.Entries[0]
|
||||
$alteredMode = (([long]$alteredEntry.ExternalAttributes -shr 16) -band 0xFFFF)
|
||||
if ($alteredMode -ne 0x81A0) { throw 'Altered ZIP fixture does not contain the intended unsafe mode.' }
|
||||
$rejected = $false
|
||||
try { Assert-ReceiverBundleEntryMode -Entry $alteredArchive.Entries[0] }
|
||||
catch { $rejected = $true }
|
||||
try { Assert-ReceiverBundleEntryMode -Entry $alteredEntry }
|
||||
catch {
|
||||
if ($_.Exception.Message -notmatch 'unsafe file mode') { throw }
|
||||
$rejected = $true
|
||||
}
|
||||
if (-not $rejected) { throw 'Installer validator accepted an altered ZIP entry mode.' }
|
||||
} finally { $alteredArchive.Dispose() }
|
||||
} finally { $alteredStream.Dispose() }
|
||||
|
||||
Write-Output ("PASS: installer validator accepted the actual packager bundle for commit {0} (SHA-256 {1}) and rejected altered mode." -f $fixture.commit, $fixture.sha256)
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force
|
||||
$expectedRoot = [IO.Path]::Combine($temporaryBase, $temporaryName)
|
||||
$rootItem = Get-Item -LiteralPath $expectedRoot -Force
|
||||
$resolvedRoot = [IO.Path]::GetFullPath((Resolve-Path -LiteralPath $expectedRoot).Path).TrimEnd('\')
|
||||
if ([string]::Compare($resolvedRoot, $expectedRoot, [StringComparison]::OrdinalIgnoreCase) -ne 0 -or
|
||||
[string]::Compare([IO.Path]::GetDirectoryName($resolvedRoot), $temporaryBase,
|
||||
[StringComparison]::OrdinalIgnoreCase) -ne 0 -or
|
||||
[string]::Compare([IO.Path]::GetFileName($resolvedRoot), $temporaryName,
|
||||
[StringComparison]::OrdinalIgnoreCase) -ne 0 -or
|
||||
($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or
|
||||
$rootItem.CreationTimeUtc -ne $temporaryRootCreated) {
|
||||
throw 'Refusing to remove a replaced, moved, or reparse temporary fixture directory.'
|
||||
}
|
||||
$aclOwner = (Get-Acl -LiteralPath $expectedRoot).Owner
|
||||
$ownerSid = ([Security.Principal.NTAccount]$aclOwner).Translate(
|
||||
[Security.Principal.SecurityIdentifier]).Value
|
||||
if ($ownerSid -cne $temporaryOwnerSid -or $ownerSid -cne $currentSid) {
|
||||
throw 'Refusing to remove a temporary fixture directory with a changed owner.'
|
||||
}
|
||||
foreach ($item in Get-ChildItem -LiteralPath $expectedRoot -Force -Recurse) {
|
||||
$itemPath = [IO.Path]::GetFullPath($item.FullName)
|
||||
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or
|
||||
-not $itemPath.StartsWith($resolvedRoot + '\', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw 'Refusing to recursively remove a reparse or out-of-root fixture entry.'
|
||||
}
|
||||
}
|
||||
[IO.Directory]::Delete($resolvedRoot, $true)
|
||||
}
|
||||
@@ -255,6 +255,15 @@ class WindowsReleaseBootstrapTests(unittest.TestCase):
|
||||
self.assertIn("$current = $current.Parent", script)
|
||||
self.assertIn("$ownerSid -notin @('S-1-5-18', 'S-1-5-32-544', $trustedInstaller)", script)
|
||||
|
||||
def test_windows_bundle_mode_harness_uses_safe_python_and_validates_cleanup_target(self):
|
||||
harness = (ROOT / "infra/deploy/test_windows_release_bundle_mode.ps1").read_text(encoding="utf-8")
|
||||
self.assertIn("-B -E -s -S", harness)
|
||||
self.assertIn("$rootItem.CreationTimeUtc -ne $temporaryRootCreated", harness)
|
||||
self.assertIn("$ownerSid -cne $temporaryOwnerSid", harness)
|
||||
self.assertIn("[IO.FileAttributes]::ReparsePoint", harness)
|
||||
self.assertIn("[IO.Directory]::Delete($resolvedRoot, $true)", harness)
|
||||
self.assertIn("-not $itemPath.StartsWith($resolvedRoot + '\\'", harness)
|
||||
|
||||
def test_windows_tool_environment_discards_path_and_git_overrides(self):
|
||||
environment = {
|
||||
"PATH": r"C:\Users\attacker\bin",
|
||||
|
||||
Reference in new issue
Block a user