Skip to content

Commit 303404a

Browse files
committed
security: validate update asset URLs
1 parent 6262a79 commit 303404a

2 files changed

Lines changed: 104 additions & 3 deletions

File tree

powershell/DevNav.psm1

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,35 @@ function Get-DevLatestRelease {
141141
}
142142
}
143143

144+
function Get-DevReleaseTag {
145+
param([Parameter(Mandatory)][string] $Tag)
146+
147+
if ($Tag -notmatch '^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$') {
148+
throw "La release publicada tiene un tag no compatible: $Tag"
149+
}
150+
return $Tag
151+
}
152+
153+
function Get-DevReleaseAssetUrl {
154+
param(
155+
[Parameter(Mandatory)][string] $Tag,
156+
[Parameter(Mandatory)][string] $AssetName
157+
)
158+
159+
$allowedAssets = @(
160+
'dev-windows-x86_64.exe',
161+
'dev-windows-aarch64.exe',
162+
'DevNav.psm1',
163+
'SHA256SUMS.txt'
164+
)
165+
if ($AssetName -notin $allowedAssets) {
166+
throw "El asset de release no está permitido: $AssetName"
167+
}
168+
169+
$releaseTag = Get-DevReleaseTag -Tag $Tag
170+
return "https://github.com/$script:DevNavRepository/releases/download/$releaseTag/$AssetName"
171+
}
172+
144173
function Invoke-DevDownload {
145174
[CmdletBinding()]
146175
param(
@@ -212,7 +241,8 @@ function Invoke-DevStartupUpdateCheck {
212241
try {
213242
$installedVersion = Get-DevInstalledVersion
214243
$release = Get-DevLatestRelease -TimeoutSeconds 5
215-
$latestText = ([string]$release.tag_name).TrimStart('v')
244+
$releaseTag = Get-DevReleaseTag -Tag ([string]$release.tag_name)
245+
$latestText = $releaseTag.Substring(1)
216246
$latestVersion = [version]$latestText
217247
}
218248
catch {
@@ -367,7 +397,8 @@ function Update-DevNavigator {
367397
Write-Host 'Comprobando la última versión publicada...'
368398
}
369399
$release = Get-DevLatestRelease
370-
$latestText = ([string]$release.tag_name).TrimStart('v')
400+
$releaseTag = Get-DevReleaseTag -Tag ([string]$release.tag_name)
401+
$latestText = $releaseTag.Substring(1)
371402
$latestVersion = [version]$latestText
372403
if ((Get-DevLanguage) -eq 'en-US') { Write-Host "Latest published: v$latestText" }
373404
else { Write-Host "Última publicada: v$latestText" }
@@ -396,7 +427,7 @@ function Update-DevNavigator {
396427
foreach ($assetName in $requiredAssets) {
397428
$asset = $releaseAssets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1
398429
if (-not $asset) { throw "La release v$latestText no contiene $assetName." }
399-
$downloads[$assetName] = $asset.browser_download_url
430+
$downloads[$assetName] = Get-DevReleaseAssetUrl -Tag $releaseTag -AssetName $assetName
400431
}
401432

402433
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("devnav-update-{0}" -f [guid]::NewGuid().ToString('N'))

tests/powershell/DevNav.Tests.ps1

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,76 @@ Describe 'DevNav updater lifecycle' {
117117
Get-ChildItem $installRoot -Filter '*.bak' | Should -BeNullOrEmpty
118118
}
119119

120+
It 'downloads x64 release assets only from canonical GitHub URLs' {
121+
$global:DevNavDownloadUris = [System.Collections.Generic.List[string]]::new()
122+
$devModule.Invoke({
123+
Mock Get-DevInstalledVersion { $global:DevNavTestPreviousVersion }
124+
Mock Get-DevLatestRelease { $global:DevNavTestRelease }
125+
Mock Invoke-WebRequest {
126+
$global:DevNavDownloadUris.Add($Uri)
127+
Copy-Item (Join-Path $global:DevNavTestSourceRoot ([IO.Path]::GetFileName(([uri]$Uri).AbsolutePath))) $OutFile
128+
}
129+
Update-DevNavigator -Confirm:$false
130+
})
131+
$expectedBase = "https://github.com/JacobOptimiza/dev-nav/releases/download/v$($global:DevNavTestRelease.tag_name.TrimStart('v'))"
132+
$global:DevNavDownloadUris | Should -Be @(
133+
"$expectedBase/dev-windows-x86_64.exe",
134+
"$expectedBase/DevNav.psm1",
135+
"$expectedBase/SHA256SUMS.txt"
136+
)
137+
}
138+
139+
It 'constructs the canonical ARM64 release asset URL' {
140+
$devModule.Invoke({
141+
Get-DevReleaseAssetUrl -Tag 'v0.13.0' -AssetName 'dev-windows-aarch64.exe'
142+
}) | Should -Be 'https://github.com/JacobOptimiza/dev-nav/releases/download/v0.13.0/dev-windows-aarch64.exe'
143+
}
144+
145+
It 'rejects a release tag outside the supported vMAJOR.MINOR.PATCH format before downloading' {
146+
$invalidRelease = [pscustomobject]@{
147+
tag_name = 'v0.13.0-preview'
148+
assets = $global:DevNavTestRelease.assets
149+
}
150+
$global:DevNavInvalidRelease = $invalidRelease
151+
$global:DevNavDownloadAttempted = $false
152+
$devModule.Invoke({
153+
Mock Get-DevInstalledVersion { $global:DevNavTestPreviousVersion }
154+
Mock Get-DevLatestRelease { $global:DevNavInvalidRelease }
155+
Mock Invoke-WebRequest { $global:DevNavDownloadAttempted = $true }
156+
{ Update-DevNavigator -Confirm:$false } | Should -Throw '*tag no compatible*'
157+
})
158+
$global:DevNavDownloadAttempted | Should -BeFalse
159+
}
160+
161+
It 'ignores malicious metadata URLs and never constructs a download for an unallowlisted asset' {
162+
$global:DevNavDownloadUris = [System.Collections.Generic.List[string]]::new()
163+
$maliciousRelease = [pscustomobject]@{
164+
tag_name = $global:DevNavTestRelease.tag_name
165+
assets = @(
166+
[pscustomobject]@{name = 'dev-windows-x86_64.exe'; browser_download_url = 'https://evil.example/payload.exe'},
167+
[pscustomobject]@{name = 'DevNav.psm1'; browser_download_url = 'http://github.com/DevNav.psm1'},
168+
[pscustomobject]@{name = 'SHA256SUMS.txt'; browser_download_url = 'https://github.com.evil.example/SHA256SUMS.txt'},
169+
[pscustomobject]@{name = 'payload.exe'; browser_download_url = 'https://github.com/other/repo/releases/download/v9.9.9/payload.exe'}
170+
)
171+
}
172+
$global:DevNavMaliciousRelease = $maliciousRelease
173+
$devModule.Invoke({
174+
Mock Get-DevInstalledVersion { $global:DevNavTestPreviousVersion }
175+
Mock Get-DevLatestRelease { $global:DevNavMaliciousRelease }
176+
Mock Invoke-WebRequest {
177+
$global:DevNavDownloadUris.Add($Uri)
178+
Copy-Item (Join-Path $global:DevNavTestSourceRoot ([IO.Path]::GetFileName(([uri]$Uri).AbsolutePath))) $OutFile
179+
}
180+
Update-DevNavigator -Confirm:$false
181+
{ Get-DevReleaseAssetUrl -Tag 'v0.13.0' -AssetName 'payload.exe' } | Should -Throw '*no está permitido*'
182+
})
183+
$global:DevNavDownloadUris | Should -Not -Contain 'https://evil.example/payload.exe'
184+
$global:DevNavDownloadUris | Should -Not -Contain 'http://github.com/DevNav.psm1'
185+
$global:DevNavDownloadUris | Should -Not -Contain 'https://github.com.evil.example/SHA256SUMS.txt'
186+
$global:DevNavDownloadUris | Should -Not -Match 'other/repo|payload.exe'
187+
@($global:DevNavDownloadUris | Where-Object { $_ -notmatch '^https://github\.com/JacobOptimiza/dev-nav/releases/download/v\d+\.\d+\.\d+/(dev-windows-x86_64\.exe|DevNav\.psm1|SHA256SUMS\.txt)$' }).Count | Should -Be 0
188+
}
189+
120190
It 'marks restart required when the module changes' {
121191
Add-Content -LiteralPath (Join-Path $sourceRoot 'DevNav.psm1') -Value "`n# update marker"
122192
$moduleHash = (Get-FileHash (Join-Path $sourceRoot 'DevNav.psm1') -Algorithm SHA256).Hash

0 commit comments

Comments
 (0)