Skip to content
Merged
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
46 changes: 40 additions & 6 deletions scripts/install.ps1
Original file line numberDiff line numberDiff line change
Expand Up@@ -275,11 +275,21 @@ function Resolve-Tag {
# Follow the /releases/latest redirect to find the tag, same trick
# install.sh uses. -MaximumRedirection 0 makes Invoke-WebRequest
# surface the Location header instead of following.
#
# This is the one fetch that stays a plain Invoke-WebRequest, not a
# Save-BoundedFile: it is a header-only redirect probe (-MaximumRedirection 0
# reads the Location header, never a body to disk), so the size ceiling that
# backend#2544 added to the artifact fetches has nothing to bound here. The
# STALL half still applies, though, so pin -TimeoutSec 60 to match the rest of
# the script rather than inherit the ~100 s platform default (LukasWodka, #588).
# On timeout the WebException is caught below, $resp is $null, and Resolve-Tag
# falls through to a clean Fail — never a hang.
try {
$resp = Invoke-WebRequest `
-Uri "https://github.com/$script:GitHubRepo/releases/latest" `
-MaximumRedirection 0 `
-UseBasicParsing `
-TimeoutSec 60 `
-ErrorAction SilentlyContinue
} catch {
# PowerShell treats 3xx as an error when MaximumRedirection=0.
Expand DownExpand Up@@ -311,11 +321,30 @@ $baseUrl = "https://github.com/$GitHubRepo/releases/download/$tag"
$tmpDir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "tracebloc-install-$tag-$([guid]::NewGuid())") -Force

try {
Write-Host "Downloading binary..."
Invoke-WebRequest -Uri "$baseUrl/$binaryFile" -OutFile (Join-Path $tmpDir $binaryFile) -UseBasicParsing
# Bounded, exactly like the cosign bootstrap above (backend#2544 finishes
# what backend#2199 started). These artifacts are SHA256- and cosign-verified
# AFTER download, so a corrupted or oversized body is rejected before use —
# but an unbounded one can still hang the install or exhaust disk BEFORE that
# verification runs, the same DoS the bootstrap fetch closed. So every fetch
# goes through Save-BoundedFile (size ceiling + timeout) rather than a raw
# Invoke-WebRequest. Caps are sized against the real published assets with
# headroom for growth; REVISIT the binary cap as the CLI grows:
# binary ~50 MB today (windows-amd64 is 49.9 MB at v0.10.x) → 200 MB
# SHA256SUMS <1 KB → a tight 1 MB / 60 s, mirroring cosign_checksums.txt
# Wrapped so a bounded-fetch throw — cap tripped, timeout, 404, oversized
# body — surfaces as Fail's clean one-line error, not the raw PowerShell
# error record + stack trace that an uncaught throw under `Stop` produces.
# These two are mandatory (unlike cosign, there is no fallback), so the
# right response is a clean hard failure, not the bootstrap's warn-and-continue.
try {
Write-Host "Downloading binary..."
Save-BoundedFile -Uri "$baseUrl/$binaryFile" -OutFile (Join-Path $tmpDir $binaryFile) -MaxBytes 200MB

Write-Host "Downloading SHA256SUMS..."
Invoke-WebRequest -Uri "$baseUrl/SHA256SUMS" -OutFile (Join-Path $tmpDir 'SHA256SUMS') -UseBasicParsing
Write-Host "Downloading SHA256SUMS..."
Save-BoundedFile -Uri "$baseUrl/SHA256SUMS" -OutFile (Join-Path $tmpDir 'SHA256SUMS') -MaxBytes 1MB -TimeoutSec 60
} catch {
Fail "couldn't download the release artifacts for ${tag}: $($_.Exception.Message)"
}

# -------------------------------------------------------------
# Verify SHA256.
Expand DownExpand Up@@ -400,8 +429,13 @@ try {
# process, whose non-zero $LASTEXITCODE cannot be caught anyway.
$sigDownloaded = $false
try {
Invoke-WebRequest -Uri "$baseUrl/$binaryFile.sig" -OutFile (Join-Path $tmpDir "$binaryFile.sig") -UseBasicParsing
Invoke-WebRequest -Uri "$baseUrl/$binaryFile.cert" -OutFile (Join-Path $tmpDir "$binaryFile.cert") -UseBasicParsing
# Bounded too (backend#2544). Both are tiny — .sig ~96 B, .cert ~3.2 KB
# — so a 1 MB / 60 s profile bounds a runaway with vast headroom. A
# Save-BoundedFile throw (cap tripped, timeout, 404, oversized body)
# lands in the same catch that already handles a missing .sig/.cert:
# fail closed unless TRACEBLOC_ALLOW_UNVERIFIED=1.
Save-BoundedFile -Uri "$baseUrl/$binaryFile.sig" -OutFile (Join-Path $tmpDir "$binaryFile.sig") -MaxBytes 1MB -TimeoutSec 60
Save-BoundedFile -Uri "$baseUrl/$binaryFile.cert" -OutFile (Join-Path $tmpDir "$binaryFile.cert") -MaxBytes 1MB -TimeoutSec 60
$sigDownloaded = $true
} catch {
if (-not $AllowUnverified) {
Expand Down
40 changes: 40 additions & 0 deletions scripts/tests/install-ps1-functions.tests.ps1
Original file line numberDiff line numberDiff line change
Expand Up@@ -328,6 +328,46 @@ if ($backslashDollar.Count -eq 0) {
$backslashDollar | ForEach-Object { bad "bash-style \$ escape in: $($_.Extent.Text)" }
}

# ── 8. every artifact fetched to disk is size-bounded (backend#2544) ────────
# backend#2199 bounded the cosign bootstrap. #2544 bounds the four artifacts
# fetched AFTER it — the CLI binary, SHA256SUMS, and the .sig/.cert — which were
# still raw Invoke-WebRequest: verified after download, but an unbounded body can
# hang the install or exhaust disk BEFORE that check runs.
#
# Save-BoundedFile's byte ceiling is behaviourally driven above (section 2b); the
# NEW surface here is the call sites, which are inline in the main script body
# and can't be extracted and run without the network. So they're asserted
# structurally against the AST — robust to reformatting, unlike a grep:
# (a) the anti-pattern is gone: nothing writes a body to a file via
# Invoke-WebRequest -OutFile anymore. This is the sharp invariant — it
# fails the moment someone adds the next raw fetch. Resolve-Tag's redirect
# probe stays legal: it reads a Location header, never passes -OutFile.
# (b) the mechanism is intact: every Save-BoundedFile call carries an explicit
# -MaxBytes, so a fetch routed through the helper can't skip the ceiling.
function Test-HasParam($cmdAst, [string]$name) {
return @($cmdAst.CommandElements | Where-Object {
$_ -is [System.Management.Automation.Language.CommandParameterAst] -and
$_.ParameterName -eq $name
}).Count -gt 0
}
$iwrToFile = @($ast.FindAll({
param($n)
$n -is [System.Management.Automation.Language.CommandAst] -and
$n.GetCommandName() -eq 'Invoke-WebRequest'
}, $true) | Where-Object { Test-HasParam $_ 'OutFile' })
is 'no artifact is fetched to disk with a raw Invoke-WebRequest -OutFile' $iwrToFile.Count 0

$sbfCalls = @($ast.FindAll({
param($n)
$n -is [System.Management.Automation.Language.CommandAst] -and
$n.GetCommandName() -eq 'Save-BoundedFile'
}, $true))
# Non-vacuity: the -MaxBytes assertion below is only meaningful if call sites
# exist. Two cosign-bootstrap fetches + four post-cosign artifacts = six.
is 'Save-BoundedFile is used at every artifact fetch (>= 6 call sites)' ($sbfCalls.Count -ge 6) $true
$sbfNoCap = @($sbfCalls | Where-Object { -not (Test-HasParam $_ 'MaxBytes') })
is 'every Save-BoundedFile call site carries an explicit -MaxBytes ceiling' $sbfNoCap.Count 0

Write-Host ''
Write-Host "install-ps1-functions: $script:Pass passed, $script:Fail failed"
if ($script:Fail -gt 0) { exit 1 }
Expand Down
26 changes: 26 additions & 0 deletions scripts/tests/install-ps1-verify.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,32 @@ else
bad 'a cosign bootstrap fetch is missing its size cap'
fi

# ── 5c. the post-cosign artifact fetches are size-bounded too (backend#2544) ─
# #2199/5b bounded only the cosign bootstrap — the pre-auth trust root. The four
# artifacts fetched AFTER it (binary, SHA256SUMS, .sig, .cert) were still raw
# Invoke-WebRequest: they are SHA256- and cosign-verified after download, but an
# unbounded body can hang the install or exhaust disk BEFORE that check runs —
# the same DoS. The sharp invariant that keeps them bounded, and catches the next
# raw fetch someone adds: NOTHING is written to a file with Invoke-WebRequest
# -OutFile anymore. (Resolve-Tag's redirect probe has no -OutFile — it reads a
# Location header, not a body — so it is correctly not caught here.)
if grep -Eq 'Invoke-WebRequest.*-OutFile' "$INSTALLER"; then
bad 'an artifact is still fetched to disk with a raw Invoke-WebRequest -OutFile'
else
ok 'no artifact is fetched to disk with a raw Invoke-WebRequest -OutFile'
fi
# …and each of the four goes through the bounded helper with an explicit cap.
# The closing quote in each pattern pins the exact asset — "$baseUrl/$binaryFile"
# must not also match the "$binaryFile.sig" / ".cert" lines.
if grep -Eq 'Save-BoundedFile.*"\$baseUrl/\$binaryFile".*-MaxBytes' "$INSTALLER" \
&& grep -Eq 'Save-BoundedFile.*"\$baseUrl/SHA256SUMS".*-MaxBytes' "$INSTALLER" \
&& grep -Eq 'Save-BoundedFile.*"\$baseUrl/\$binaryFile\.sig".*-MaxBytes' "$INSTALLER" \
&& grep -Eq 'Save-BoundedFile.*"\$baseUrl/\$binaryFile\.cert".*-MaxBytes' "$INSTALLER"; then
ok 'binary, SHA256SUMS, .sig and .cert are all size-capped via the bounded helper'
else
bad 'a post-cosign artifact fetch is missing its size cap'
fi

# ── 6. behavioural tier ─────────────────────────────────────────────────────
# pwsh is preinstalled on GitHub-hosted ubuntu runners. If it is missing we
# cannot tell whether the helpers behave, and "cannot tell" is a finding, not a
Expand Down
Loading