From 2f0ff9085ae53a13303bd4dc3725be973fe1ef89 Mon Sep 17 00:00:00 2001 From: Tomas Kracmar Date: Wed, 29 Jul 2026 12:04:43 +0200 Subject: [PATCH] fix(Prepare-KHDBStorage): reject manifest shard names outside shard root In -UploadOnly mode, entry.name from the parsed manifest.json was used directly in Join-Path (local read) and as the remote object key with no path-traversal check. A tampered local manifest.json (requires prior local write access to the shard directory) with a name like '..\..\secrets.txt' could make the upload step read and upload an arbitrary local file under an attacker-chosen remote key. Now resolves each shard's full path via GetFullPath and rejects any entry that resolves outside the shard root, before it's added to the list actually used for reads/uploads. Verified the resolved-path containment check against normal relative names (allowed) and '..' traversal attempts (blocked). Co-Authored-By: Claude Sonnet 5 --- Prepare-KHDBStorage.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Prepare-KHDBStorage.ps1 b/Prepare-KHDBStorage.ps1 index 829b665..fdfddcf 100644 --- a/Prepare-KHDBStorage.ps1 +++ b/Prepare-KHDBStorage.ps1 @@ -892,12 +892,21 @@ if ($UploadOnly) { $manifestHash = (Get-FileHash -Path $manifestPath -Algorithm SHA256).Hash.ToLowerInvariant() + $resolvedLocalShardRoot = [System.IO.Path]::GetFullPath($localShardRoot).TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar + $manifestShards = @() $totalSizeBytes = 0L foreach ($entry in ($manifestObject.shards | Sort-Object name)) { $name = [string]$entry.name if ([string]::IsNullOrWhiteSpace($name)) { continue } $localPath = Join-Path -Path $localShardRoot -ChildPath $name + # Manifest shard names are attacker-controllable if manifest.json was tampered with (requires + # prior local write access to the shard directory). Reject anything that resolves outside the + # shard root instead of trusting Join-Path to keep '..'/rooted paths contained. + $resolvedLocalPath = [System.IO.Path]::GetFullPath($localPath) + if (-not $resolvedLocalPath.StartsWith($resolvedLocalShardRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Manifest shard name '$name' resolves outside the shard directory '$localShardRoot'." + } if (-not (Test-Path -LiteralPath $localPath)) { throw "Shard file '$name' listed in manifest was not found under '$localShardRoot'." }