From 7e80f7a09137c95a28ba3a85e1104dcd38d26477 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 16 Sep 2026 00:44:06 +0100 Subject: [PATCH] fix: check the uv installer against a pinned sha256 before running it Both installers download the pinned uv installer to a file and run it only when its sha256 matches the value pinned next to UV_VERSION; bumping the version means bumping the hash. Astral publishes checksums for the uv binaries but not for the installer scripts, so the hash is pinned here. get.docker.com is still only downloaded in full before running: its content changes over time and it publishes no checksum. --- deployment/install.ps1 | 20 +++++++++++++++++--- deployment/install.sh | 35 +++++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/deployment/install.ps1 b/deployment/install.ps1 index e8ea9a96..f62edc70 100644 --- a/deployment/install.ps1 +++ b/deployment/install.ps1 @@ -23,6 +23,9 @@ function Install-DocsGPT { $ErrorActionPreference = 'Stop' $UvVersion = '0.12.15' $UvMinVersion = [version]'0.8.0' + # sha256 of https://astral.sh/uv/$UvVersion/install.ps1, checked before it runs. Bump it with + # $UvVersion: (Invoke-WebRequest "https://astral.sh/uv//install.ps1").Content | … + $UvInstallerSha256 = '63f2d7e2ccc347cc018127b22f56f569080e5730a390dcb702b654daf6822193' function Say([string]$Message) { Write-Host "==> $Message" } @@ -50,9 +53,20 @@ function Install-DocsGPT { $env:UV_INSTALL_DIR = $uvDir $env:UV_NO_MODIFY_PATH = '1' $env:UV_PRINT_QUIET = '1' - # A child PowerShell, so nothing the uv installer does can end this session. - $shell = (Get-Process -Id $PID).Path - & $shell -NoProfile -ExecutionPolicy Bypass -Command "irm https://astral.sh/uv/$UvVersion/install.ps1 | iex" + # Downloaded and checked before it runs, and run in a child PowerShell so nothing the + # uv installer does can end this session. + $installer = Join-Path ([System.IO.Path]::GetTempPath()) "uv-installer-$UvVersion.ps1" + try { + Invoke-WebRequest -Uri "https://astral.sh/uv/$UvVersion/install.ps1" -OutFile $installer -UseBasicParsing + $actual = (Get-FileHash -Path $installer -Algorithm SHA256).Hash.ToLower() + if ($actual -ne $UvInstallerSha256) { + throw "The uv installer does not match its pinned sha256: got $actual, expected $UvInstallerSha256. Refusing to run it." + } + $shell = (Get-Process -Id $PID).Path + & $shell -NoProfile -ExecutionPolicy Bypass -File $installer + } finally { + Remove-Item $installer -Force -ErrorAction SilentlyContinue + } $uv = Join-Path $uvDir 'uv.exe' if (-not (Test-Path $uv)) { throw "uv did not install into $uvDir" } } diff --git a/deployment/install.sh b/deployment/install.sh index 018bb761..c7b81dee 100755 --- a/deployment/install.sh +++ b/deployment/install.sh @@ -20,6 +20,9 @@ UV_VERSION="0.12.15" UV_MIN_VERSION="0.8.0" +# sha256 of https://astral.sh/uv/$UV_VERSION/install.sh, checked before it runs. Bump it with +# UV_VERSION: curl -fsSL https://astral.sh/uv//install.sh | shasum -a 256 +UV_INSTALLER_SHA256="716a1d6844740756c68770fcec2f79c2013fb9b03869a113f61e15f6f482a6a1" main() { set -euo pipefail @@ -47,16 +50,33 @@ main() { die "curl or wget is needed to download $1" fi } - # run_downloaded URL COMMAND...: save URL to a file, then run COMMAND with the file as its last - # argument. A transfer cut short fails before anything runs. + sha256_of() { + if has shasum; then + shasum -a 256 "$1" | awk '{print $1}' + elif has sha256sum; then + sha256sum "$1" | awk '{print $1}' + else + die "neither shasum nor sha256sum is available to check $1" + fi + } + # run_downloaded URL SHA256 COMMAND...: save URL to a file, check it against SHA256 ("-" to + # skip), then run COMMAND with the file as its last argument. A transfer cut short, or content + # that does not match, fails before anything runs. run_downloaded() { - local url="$1" script status=0 - shift + local url="$1" expected="$2" script status=0 actual + shift 2 script="$(mktemp)" if ! download "$url" >"$script"; then rm -f "$script" die "could not download $url" fi + if [ "$expected" != "-" ]; then + actual="$(sha256_of "$script")" + if [ "$actual" != "$expected" ]; then + rm -f "$script" + die "$url does not match its pinned sha256: got $actual, expected $expected. Refusing to run it." + fi + fi "$@" "$script" || status=$? rm -f "$script" return "$status" @@ -96,7 +116,9 @@ main() { sudo="sudo" fi say "Installing Docker" - run_downloaded https://get.docker.com $sudo sh + # Docker's script changes over time and publishes no checksum, so it is only downloaded + # in full before it runs. + run_downloaded https://get.docker.com - $sudo sh $sudo systemctl enable --now docker >/dev/null 2>&1 || true if [ -n "$sudo" ]; then $sudo usermod -aG docker "$(id -un)" @@ -122,7 +144,8 @@ main() { if [ -z "$uv" ]; then local uv_dir="${XDG_BIN_HOME:-$HOME/.local/bin}" say "Installing uv $UV_VERSION into $uv_dir" - run_downloaded "https://astral.sh/uv/$UV_VERSION/install.sh" env UV_INSTALL_DIR="$uv_dir" UV_NO_MODIFY_PATH=1 UV_PRINT_QUIET=1 sh + run_downloaded "https://astral.sh/uv/$UV_VERSION/install.sh" "$UV_INSTALLER_SHA256" \ + env UV_INSTALL_DIR="$uv_dir" UV_NO_MODIFY_PATH=1 UV_PRINT_QUIET=1 sh uv="$uv_dir/uv" [ -x "$uv" ] || die "uv did not install into $uv_dir" fi