From 86a80ec2e1c29b368b0337e149bd952d7275a729 Mon Sep 17 00:00:00 2001 From: unknown <49066403+bodane@users.noreply.github.com> Date: Sun, 1 Feb 2026 00:00:23 +1100 Subject: [PATCH 01/10] fix: Windows compatibility for install scripts - Use Join-Path for reliable path handling in PowerShell - Replace fragile New-TemporaryFile with GetTempPath + GUID - Enable delayed expansion in batch for reliable errorlevel checks - Add call statements for proper subprocess error propagation - Verify core.exe exists before running commands - Update repo reference and build path Co-Authored-By: Claude Opus 4.5 --- scripts/install-core.ps1 | 16 +++++++++------- setup.bat | 25 +++++++++++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/scripts/install-core.ps1 b/scripts/install-core.ps1 index 950835b..2d79288 100644 --- a/scripts/install-core.ps1 +++ b/scripts/install-core.ps1 @@ -3,7 +3,7 @@ $ErrorActionPreference = "Stop" -$Repo = "Snider/Core" +$Repo = "host-uk/core" $InstallDir = "$env:LOCALAPPDATA\Programs\core" function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Green } @@ -17,7 +17,7 @@ function Test-Command($cmd) { # Download pre-built binary function Download-Binary { $arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "386" } - $url = "https://github.com/$Repo/releases/latest/download/core-windows-$arch.exe" + $url = "https://github.com/$Repo/releases/download/dev/core-windows-$arch.exe" Write-Info "Attempting to download pre-built binary..." Write-Info "URL: $url" @@ -39,18 +39,20 @@ function Build-FromSource { Write-Err "Go is required to build from source. Run '.\scripts\install-deps.ps1' first" } - $tmpdir = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } + $tmpdir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Force -Path $tmpdir | Out-Null Write-Info "Cloning $Repo..." - git clone --depth 1 "https://github.com/$Repo.git" "$tmpdir\Core" + $cloneDir = Join-Path $tmpdir "Core" + git clone --depth 1 "https://github.com/$Repo.git" $cloneDir Write-Info "Building core CLI..." - Push-Location "$tmpdir\Core" - go build -o core.exe ./cmd/core + Push-Location $cloneDir + go build -o core.exe . Pop-Location New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null - Move-Item "$tmpdir\Core\core.exe" "$InstallDir\core.exe" -Force + Move-Item (Join-Path $cloneDir "core.exe") (Join-Path $InstallDir "core.exe") -Force Remove-Item -Recurse -Force $tmpdir Write-Info "Built and installed to $InstallDir\core.exe" diff --git a/setup.bat b/setup.bat index d728f71..49ca5b4 100644 --- a/setup.bat +++ b/setup.bat @@ -1,13 +1,14 @@ @echo off +setlocal enabledelayedexpansion REM Quick setup script for Windows -REM Run as Administrator: .\setup.bat +REM Run as Administrator: setup.bat echo === Host UK Developer Workspace Setup === echo. REM Check for admin rights net session >nul 2>&1 -if %errorlevel% neq 0 ( +if !errorlevel! neq 0 ( echo ERROR: Please run this script as Administrator echo Right-click and select "Run as administrator" pause @@ -16,36 +17,44 @@ if %errorlevel% neq 0 ( REM Install dependencies echo Installing dependencies... -powershell -ExecutionPolicy Bypass -File "%~dp0scripts\install-deps.ps1" -if %errorlevel% neq 0 goto :error +call powershell -ExecutionPolicy Bypass -File "%~dp0scripts\install-deps.ps1" +if !errorlevel! neq 0 goto :error REM Install core CLI echo. echo Installing core CLI... -powershell -ExecutionPolicy Bypass -File "%~dp0scripts\install-core.ps1" -if %errorlevel% neq 0 goto :error +call powershell -ExecutionPolicy Bypass -File "%~dp0scripts\install-core.ps1" +if !errorlevel! neq 0 goto :error REM Refresh PATH set "PATH=%PATH%;%LOCALAPPDATA%\Programs\core" +REM Verify core.exe exists before running +if not exist "%LOCALAPPDATA%\Programs\core\core.exe" ( + echo ERROR: core.exe not found at %LOCALAPPDATA%\Programs\core\core.exe + goto :error +) + REM Run doctor echo. echo === Verifying environment === -core doctor +call "%LOCALAPPDATA%\Programs\core\core.exe" doctor REM Clone repos echo. echo === Cloning repositories === -core setup +call "%LOCALAPPDATA%\Programs\core\core.exe" setup echo. echo === Setup complete! === echo Run 'core health' to check status pause +endlocal exit /b 0 :error echo. echo Setup failed! Check the error above. pause +endlocal exit /b 1 From 2e034f43f22322e53338d68775e8f1e282ac0634 Mon Sep 17 00:00:00 2001 From: unknown <49066403+bodane@users.noreply.github.com> Date: Sun, 1 Feb 2026 00:07:45 +1100 Subject: [PATCH 02/10] fix: add error handling and cleanup to install scripts - Add git prerequisite check before cloning - Check $LASTEXITCODE after git clone and go build - Wrap build logic in try/finally for guaranteed temp cleanup - Add error check after core setup command in batch file - Show warning if core doctor reports issues Co-Authored-By: Claude Opus 4.5 --- scripts/install-core.ps1 | 37 ++++++++++++++++++++++++++----------- setup.bat | 4 ++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/scripts/install-core.ps1 b/scripts/install-core.ps1 index 2d79288..463cca0 100644 --- a/scripts/install-core.ps1 +++ b/scripts/install-core.ps1 @@ -35,6 +35,9 @@ function Download-Binary { # Build from source function Build-FromSource { + if (-not (Test-Command git)) { + Write-Err "Git is required to build from source. Run '.\scripts\install-deps.ps1' first" + } if (-not (Test-Command go)) { Write-Err "Go is required to build from source. Run '.\scripts\install-deps.ps1' first" } @@ -42,20 +45,32 @@ function Build-FromSource { $tmpdir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) New-Item -ItemType Directory -Force -Path $tmpdir | Out-Null - Write-Info "Cloning $Repo..." - $cloneDir = Join-Path $tmpdir "Core" - git clone --depth 1 "https://github.com/$Repo.git" $cloneDir + try { + Write-Info "Cloning $Repo..." + $cloneDir = Join-Path $tmpdir "Core" + git clone --depth 1 "https://github.com/$Repo.git" $cloneDir + if ($LASTEXITCODE -ne 0) { + Write-Err "Failed to clone repository" + } - Write-Info "Building core CLI..." - Push-Location $cloneDir - go build -o core.exe . - Pop-Location + Write-Info "Building core CLI..." + Push-Location $cloneDir + go build -o core.exe . + if ($LASTEXITCODE -ne 0) { + Pop-Location + Write-Err "Go build failed" + } + Pop-Location - New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null - Move-Item (Join-Path $cloneDir "core.exe") (Join-Path $InstallDir "core.exe") -Force + New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + Move-Item (Join-Path $cloneDir "core.exe") (Join-Path $InstallDir "core.exe") -Force - Remove-Item -Recurse -Force $tmpdir - Write-Info "Built and installed to $InstallDir\core.exe" + Write-Info "Built and installed to $InstallDir\core.exe" + } finally { + if (Test-Path $tmpdir) { + Remove-Item -Recurse -Force $tmpdir -ErrorAction SilentlyContinue + } + } } # Add to PATH diff --git a/setup.bat b/setup.bat index 49ca5b4..df65fa8 100644 --- a/setup.bat +++ b/setup.bat @@ -39,11 +39,15 @@ REM Run doctor echo. echo === Verifying environment === call "%LOCALAPPDATA%\Programs\core\core.exe" doctor +if !errorlevel! neq 0 ( + echo WARNING: core doctor reported issues +) REM Clone repos echo. echo === Cloning repositories === call "%LOCALAPPDATA%\Programs\core\core.exe" setup +if !errorlevel! neq 0 goto :error echo. echo === Setup complete! === From 53a2617fb8415a77e2abab4cf2aab4149de2cb26 Mon Sep 17 00:00:00 2001 From: unknown <49066403+bodane@users.noreply.github.com> Date: Sun, 1 Feb 2026 00:11:34 +1100 Subject: [PATCH 03/10] refactor: use try/finally for Push-Location cleanup Ensures Pop-Location is always called, even if Write-Err exits early. Makes the cleanup pattern consistent and easier to maintain. Co-Authored-By: Claude Opus 4.5 --- scripts/install-core.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/install-core.ps1 b/scripts/install-core.ps1 index 463cca0..edb43df 100644 --- a/scripts/install-core.ps1 +++ b/scripts/install-core.ps1 @@ -55,12 +55,14 @@ function Build-FromSource { Write-Info "Building core CLI..." Push-Location $cloneDir - go build -o core.exe . - if ($LASTEXITCODE -ne 0) { + try { + go build -o core.exe . + if ($LASTEXITCODE -ne 0) { + Write-Err "Go build failed" + } + } finally { Pop-Location - Write-Err "Go build failed" } - Pop-Location New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null Move-Item (Join-Path $cloneDir "core.exe") (Join-Path $InstallDir "core.exe") -Force From c27158066e1554668c61a31a726d570647b196b1 Mon Sep 17 00:00:00 2001 From: unknown <49066403+bodane@users.noreply.github.com> Date: Sun, 1 Feb 2026 00:20:37 +1100 Subject: [PATCH 04/10] security: harden install script against supply chain attacks - Add SHA256 hash verification for downloaded binaries - Pin to specific version (v0.1.0) instead of dev tag - Validate LOCALAPPDATA is within user profile - Detect symlink attacks on install directory - Set restrictive ACL (owner-only) on temp build directories - Validate PATH entries before modification - Improve error handling with specific exception types Co-Authored-By: Claude Opus 4.5 --- scripts/install-core.ps1 | 183 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 169 insertions(+), 14 deletions(-) diff --git a/scripts/install-core.ps1 b/scripts/install-core.ps1 index edb43df..daf8358 100644 --- a/scripts/install-core.ps1 +++ b/scripts/install-core.ps1 @@ -4,7 +4,7 @@ $ErrorActionPreference = "Stop" $Repo = "host-uk/core" -$InstallDir = "$env:LOCALAPPDATA\Programs\core" +$Version = "v0.1.0" # Pinned version - update when releasing new versions function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Green } function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow } @@ -14,22 +14,149 @@ function Test-Command($cmd) { return [bool](Get-Command $cmd -ErrorAction SilentlyContinue) } -# Download pre-built binary +# Validate and get secure install directory +function Get-SecureInstallDir { + # Validate LOCALAPPDATA is within user profile + $localAppData = $env:LOCALAPPDATA + $userProfile = $env:USERPROFILE + + if ([string]::IsNullOrEmpty($localAppData)) { + Write-Err "LOCALAPPDATA environment variable is not set" + } + + if ([string]::IsNullOrEmpty($userProfile)) { + Write-Err "USERPROFILE environment variable is not set" + } + + # Resolve to absolute paths + $resolvedLocalAppData = [System.IO.Path]::GetFullPath($localAppData) + $resolvedUserProfile = [System.IO.Path]::GetFullPath($userProfile) + + # Ensure LOCALAPPDATA is under user profile (prevent redirection attacks) + if (-not $resolvedLocalAppData.StartsWith($resolvedUserProfile, [System.StringComparison]::OrdinalIgnoreCase)) { + Write-Err "LOCALAPPDATA ($resolvedLocalAppData) is not within user profile ($resolvedUserProfile). Possible path manipulation detected." + } + + $installDir = Join-Path $resolvedLocalAppData "Programs\core" + + # Validate the resolved path doesn't contain suspicious patterns + if ($installDir -match '\.\.' -or $installDir -match '[\x00-\x1f]') { + Write-Err "Invalid characters detected in install path" + } + + return $installDir +} + +$InstallDir = Get-SecureInstallDir + +# Verify SHA256 hash of downloaded file +function Test-FileHash { + param( + [string]$FilePath, + [string]$ExpectedHash + ) + + $actualHash = (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash + if ($actualHash -ne $ExpectedHash) { + Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue + Write-Err "Hash verification failed! Expected: $ExpectedHash, Got: $actualHash. The downloaded file may be corrupted or tampered with." + } + Write-Info "Hash verification passed" +} + +# Download pre-built binary with integrity verification function Download-Binary { $arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "386" } - $url = "https://github.com/$Repo/releases/download/dev/core-windows-$arch.exe" + $binaryUrl = "https://github.com/$Repo/releases/download/$Version/core-windows-$arch.exe" + $checksumUrl = "https://github.com/$Repo/releases/download/$Version/checksums.txt" - Write-Info "Attempting to download pre-built binary..." - Write-Info "URL: $url" + Write-Info "Attempting to download pre-built binary (version $Version)..." + Write-Info "URL: $binaryUrl" try { New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null - Invoke-WebRequest -Uri $url -OutFile "$InstallDir\core.exe" -UseBasicParsing - Write-Info "Downloaded to $InstallDir\core.exe" + + # Verify install directory is actually a directory (not a symlink to elsewhere) + $dirInfo = Get-Item $InstallDir + if ($dirInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { + Write-Err "Install directory is a symbolic link. Possible symlink attack detected." + } + + $tempExe = Join-Path $InstallDir "core.exe.tmp" + + # Download the binary + Invoke-WebRequest -Uri $binaryUrl -OutFile $tempExe -UseBasicParsing + + # Download and parse checksums + Write-Info "Verifying download integrity..." + $checksumContent = Invoke-WebRequest -Uri $checksumUrl -UseBasicParsing | Select-Object -ExpandProperty Content + + # Parse checksum file (format: "hash filename") + $expectedHash = $null + $checksumLines = $checksumContent -split "`n" + foreach ($line in $checksumLines) { + if ($line -match "^([a-fA-F0-9]{64})\s+.*core-windows-$arch\.exe") { + $expectedHash = $matches[1].ToUpper() + break + } + } + + if ([string]::IsNullOrEmpty($expectedHash)) { + Remove-Item -Path $tempExe -Force -ErrorAction SilentlyContinue + Write-Err "Could not find checksum for core-windows-$arch.exe in checksums.txt" + } + + # Verify hash + Test-FileHash -FilePath $tempExe -ExpectedHash $expectedHash + + # Move to final location only after verification + Move-Item -Path $tempExe -Destination (Join-Path $InstallDir "core.exe") -Force + + Write-Info "Downloaded and verified: $InstallDir\core.exe" return $true - } catch { - Write-Warn "No pre-built binary available, will build from source" + } catch [System.Net.WebException] { + Write-Warn "Network error during download: $($_.Exception.Message)" + Write-Warn "Will attempt to build from source" return $false + } catch [System.IO.IOException] { + Write-Warn "File system error: $($_.Exception.Message)" + Write-Warn "Will attempt to build from source" + return $false + } catch { + Write-Warn "Download failed: $($_.Exception.Message)" + Write-Warn "Will attempt to build from source" + return $false + } +} + +# Set restrictive ACL on directory (owner-only access) +function Set-SecureDirectoryAcl { + param([string]$Path) + + try { + $acl = Get-Acl $Path + + # Disable inheritance and remove inherited rules + $acl.SetAccessRuleProtection($true, $false) + + # Clear existing rules + $acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) } | Out-Null + + # Add full control for current user only + $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $currentUser, + [System.Security.AccessControl.FileSystemRights]::FullControl, + [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit, + [System.Security.AccessControl.PropagationFlags]::None, + [System.Security.AccessControl.AccessControlType]::Allow + ) + $acl.AddAccessRule($rule) + + Set-Acl -Path $Path -AclObject $acl + Write-Info "Set restrictive permissions on $Path" + } catch { + Write-Warn "Could not set restrictive ACL: $($_.Exception.Message)" } } @@ -45,12 +172,17 @@ function Build-FromSource { $tmpdir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) New-Item -ItemType Directory -Force -Path $tmpdir | Out-Null + # Set restrictive ACL on temp directory + Set-SecureDirectoryAcl -Path $tmpdir + try { - Write-Info "Cloning $Repo..." + Write-Info "Cloning $Repo (version $Version)..." $cloneDir = Join-Path $tmpdir "Core" - git clone --depth 1 "https://github.com/$Repo.git" $cloneDir + + # Clone specific tag for reproducibility + git clone --depth 1 --branch $Version "https://github.com/$Repo.git" $cloneDir if ($LASTEXITCODE -ne 0) { - Write-Err "Failed to clone repository" + Write-Err "Failed to clone repository at version $Version" } Write-Info "Building core CLI..." @@ -65,6 +197,13 @@ function Build-FromSource { } New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + + # Verify install directory is not a symlink + $dirInfo = Get-Item $InstallDir + if ($dirInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { + Write-Err "Install directory is a symbolic link. Possible symlink attack detected." + } + Move-Item (Join-Path $cloneDir "core.exe") (Join-Path $InstallDir "core.exe") -Force Write-Info "Built and installed to $InstallDir\core.exe" @@ -75,9 +214,25 @@ function Build-FromSource { } } -# Add to PATH +# Validate and add to PATH function Setup-Path { $userPath = [Environment]::GetEnvironmentVariable("PATH", "User") + + # Validate InstallDir is a real directory under user profile + if (-not (Test-Path $InstallDir -PathType Container)) { + Write-Warn "Install directory does not exist, skipping PATH setup" + return + } + + $resolvedInstallDir = [System.IO.Path]::GetFullPath($InstallDir) + $resolvedUserProfile = [System.IO.Path]::GetFullPath($env:USERPROFILE) + + # Ensure we're only adding paths under user profile + if (-not $resolvedInstallDir.StartsWith($resolvedUserProfile, [System.StringComparison]::OrdinalIgnoreCase)) { + Write-Warn "Install directory is outside user profile, skipping PATH modification for security" + return + } + if ($userPath -notlike "*$InstallDir*") { Write-Info "Adding $InstallDir to PATH..." [Environment]::SetEnvironmentVariable("PATH", "$userPath;$InstallDir", "User") @@ -104,7 +259,7 @@ function Verify { # Main function Main { - Write-Info "Installing Core CLI..." + Write-Info "Installing Core CLI (version $Version)..." # Try download first, fallback to build if (-not (Download-Binary)) { From 991bb45d44809b6731285180d1a7cb24d3268620 Mon Sep 17 00:00:00 2001 From: unknown <49066403+bodane@users.noreply.github.com> Date: Sun, 1 Feb 2026 00:29:32 +1100 Subject: [PATCH 05/10] security: address remaining vulnerabilities from security review install-core.ps1: - Add Test-SecureDirectory and New-SecureDirectory to mitigate TOCTOU races - Add Test-GitTagSignature for GPG verification of git tags - Make ACL failures fatal for temp directories with retry logic - Use precise PATH matching instead of substring contains - Add unique GUID suffix to temp file names - Document security controls and known limitations in header setup.bat: - Validate LOCALAPPDATA is within USERPROFILE - Reject paths with invalid shell characters - Add symlink detection for install directory - Use delayed expansion variables for path safety Co-Authored-By: Claude Opus 4.5 --- scripts/install-core.ps1 | 220 +++++++++++++++++++++++++++++---------- setup.bat | 69 ++++++++++-- 2 files changed, 224 insertions(+), 65 deletions(-) diff --git a/scripts/install-core.ps1 b/scripts/install-core.ps1 index daf8358..e724d08 100644 --- a/scripts/install-core.ps1 +++ b/scripts/install-core.ps1 @@ -1,5 +1,17 @@ # Install the Core CLI (Windows) # Run: .\scripts\install-core.ps1 +# +# SECURITY CONTROLS: +# - Version pinning prevents supply chain attacks via tag manipulation +# - SHA256 hash verification ensures binary integrity +# - Path validation prevents LOCALAPPDATA redirection attacks +# - Symlink detection prevents directory traversal attacks +# - Restrictive ACLs on temp directories prevent local privilege escalation +# - GPG signature verification (when available) ensures code authenticity +# +# KNOWN LIMITATIONS: +# - Checksums are fetched from same origin as binaries (consider separate trust root) +# - No TLS certificate pinning (relies on system CA store) $ErrorActionPreference = "Stop" @@ -49,6 +61,24 @@ function Get-SecureInstallDir { $InstallDir = Get-SecureInstallDir +# Verify directory is safe for writing (not a symlink, correct permissions) +function Test-SecureDirectory { + param([string]$Path) + + if (-not (Test-Path $Path)) { + return $true # Directory doesn't exist yet, will be created + } + + $dirInfo = Get-Item $Path -Force + + # Check for symlinks/junctions + if ($dirInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { + Write-Err "Directory '$Path' is a symbolic link or junction. Possible symlink attack detected." + } + + return $true +} + # Verify SHA256 hash of downloaded file function Test-FileHash { param( @@ -61,7 +91,78 @@ function Test-FileHash { Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue Write-Err "Hash verification failed! Expected: $ExpectedHash, Got: $actualHash. The downloaded file may be corrupted or tampered with." } - Write-Info "Hash verification passed" + Write-Info "Hash verification passed (SHA256: $($actualHash.Substring(0,16))...)" +} + +# Create directory with atomic check-and-create to minimize TOCTOU window +function New-SecureDirectory { + param([string]$Path) + + # Check parent directory for symlinks first + $parent = Split-Path $Path -Parent + if ($parent -and (Test-Path $parent)) { + Test-SecureDirectory -Path $parent + } + + # Create directory + if (-not (Test-Path $Path)) { + New-Item -ItemType Directory -Force -Path $Path | Out-Null + } + + # Immediately verify it's not a symlink (minimize TOCTOU window) + Test-SecureDirectory -Path $Path + + return $Path +} + +# Set restrictive ACL on directory (owner-only access) +# This is REQUIRED for temp directories - failure is fatal +function Set-SecureDirectoryAcl { + param( + [string]$Path, + [switch]$Required + ) + + $maxRetries = 3 + $retryCount = 0 + + while ($retryCount -lt $maxRetries) { + try { + $acl = Get-Acl $Path + + # Disable inheritance and remove inherited rules + $acl.SetAccessRuleProtection($true, $false) + + # Clear existing rules + $acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) } | Out-Null + + # Add full control for current user only + $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $currentUser, + [System.Security.AccessControl.FileSystemRights]::FullControl, + [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit, + [System.Security.AccessControl.PropagationFlags]::None, + [System.Security.AccessControl.AccessControlType]::Allow + ) + $acl.AddAccessRule($rule) + + Set-Acl -Path $Path -AclObject $acl + Write-Info "Set restrictive permissions on $Path" + return $true + } catch { + $retryCount++ + if ($retryCount -ge $maxRetries) { + if ($Required) { + Write-Err "SECURITY: Failed to set restrictive ACL on '$Path' after $maxRetries attempts: $($_.Exception.Message)" + } else { + Write-Warn "Could not set restrictive ACL on '$Path': $($_.Exception.Message)" + return $false + } + } + Start-Sleep -Milliseconds 100 + } + } } # Download pre-built binary with integrity verification @@ -74,17 +175,13 @@ function Download-Binary { Write-Info "URL: $binaryUrl" try { - New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + # Create and verify install directory + New-SecureDirectory -Path $InstallDir - # Verify install directory is actually a directory (not a symlink to elsewhere) - $dirInfo = Get-Item $InstallDir - if ($dirInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { - Write-Err "Install directory is a symbolic link. Possible symlink attack detected." - } + # Use a temp file in the same directory (same filesystem for atomic move) + $tempExe = Join-Path $InstallDir "core.exe.tmp.$([System.Guid]::NewGuid().ToString('N').Substring(0,8))" - $tempExe = Join-Path $InstallDir "core.exe.tmp" - - # Download the binary + # Download the binary to temp location Invoke-WebRequest -Uri $binaryUrl -OutFile $tempExe -UseBasicParsing # Download and parse checksums @@ -106,13 +203,17 @@ function Download-Binary { Write-Err "Could not find checksum for core-windows-$arch.exe in checksums.txt" } - # Verify hash + # Verify hash BEFORE any move operation Test-FileHash -FilePath $tempExe -ExpectedHash $expectedHash - # Move to final location only after verification - Move-Item -Path $tempExe -Destination (Join-Path $InstallDir "core.exe") -Force + # Re-verify directory hasn't been replaced with symlink (reduce TOCTOU window) + Test-SecureDirectory -Path $InstallDir - Write-Info "Downloaded and verified: $InstallDir\core.exe" + # Atomic move to final location (same filesystem) + $finalPath = Join-Path $InstallDir "core.exe" + Move-Item -Path $tempExe -Destination $finalPath -Force + + Write-Info "Downloaded and verified: $finalPath" return $true } catch [System.Net.WebException] { Write-Warn "Network error during download: $($_.Exception.Message)" @@ -129,38 +230,41 @@ function Download-Binary { } } -# Set restrictive ACL on directory (owner-only access) -function Set-SecureDirectoryAcl { - param([string]$Path) +# Verify GPG signature on git tag (if gpg is available) +function Test-GitTagSignature { + param( + [string]$RepoPath, + [string]$Tag + ) + if (-not (Test-Command gpg)) { + Write-Warn "GPG not available - skipping tag signature verification" + Write-Warn "For enhanced security, install GPG and import the project signing key" + return $true # Continue without verification + } + + Push-Location $RepoPath try { - $acl = Get-Acl $Path - - # Disable inheritance and remove inherited rules - $acl.SetAccessRuleProtection($true, $false) - - # Clear existing rules - $acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) } | Out-Null - - # Add full control for current user only - $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name - $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( - $currentUser, - [System.Security.AccessControl.FileSystemRights]::FullControl, - [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit, - [System.Security.AccessControl.PropagationFlags]::None, - [System.Security.AccessControl.AccessControlType]::Allow - ) - $acl.AddAccessRule($rule) - - Set-Acl -Path $Path -AclObject $acl - Write-Info "Set restrictive permissions on $Path" - } catch { - Write-Warn "Could not set restrictive ACL: $($_.Exception.Message)" + # Attempt to verify the tag signature + $result = git tag -v $Tag 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Info "GPG signature verified for tag $Tag" + return $true + } else { + # Check if tag is unsigned vs signature invalid + if ($result -match "error: no signature found") { + Write-Warn "Tag $Tag is not signed - continuing without signature verification" + return $true + } else { + Write-Err "GPG signature verification FAILED for tag $Tag. Possible tampering detected." + } + } + } finally { + Pop-Location } } -# Build from source +# Build from source with security checks function Build-FromSource { if (-not (Test-Command git)) { Write-Err "Git is required to build from source. Run '.\scripts\install-deps.ps1' first" @@ -169,11 +273,12 @@ function Build-FromSource { Write-Err "Go is required to build from source. Run '.\scripts\install-deps.ps1' first" } - $tmpdir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) - New-Item -ItemType Directory -Force -Path $tmpdir | Out-Null + # Create secure temp directory with restrictive ACL + $tmpdir = Join-Path ([System.IO.Path]::GetTempPath()) "core-build-$([System.Guid]::NewGuid().ToString('N'))" + New-SecureDirectory -Path $tmpdir - # Set restrictive ACL on temp directory - Set-SecureDirectoryAcl -Path $tmpdir + # ACL is REQUIRED for temp build directories (security critical) + Set-SecureDirectoryAcl -Path $tmpdir -Required try { Write-Info "Cloning $Repo (version $Version)..." @@ -185,6 +290,9 @@ function Build-FromSource { Write-Err "Failed to clone repository at version $Version" } + # Verify GPG signature on tag (if available) + Test-GitTagSignature -RepoPath $cloneDir -Tag $Version + Write-Info "Building core CLI..." Push-Location $cloneDir try { @@ -196,14 +304,10 @@ function Build-FromSource { Pop-Location } - New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null - - # Verify install directory is not a symlink - $dirInfo = Get-Item $InstallDir - if ($dirInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { - Write-Err "Install directory is a symbolic link. Possible symlink attack detected." - } + # Create and verify install directory + New-SecureDirectory -Path $InstallDir + # Move built binary to install location Move-Item (Join-Path $cloneDir "core.exe") (Join-Path $InstallDir "core.exe") -Force Write-Info "Built and installed to $InstallDir\core.exe" @@ -214,7 +318,7 @@ function Build-FromSource { } } -# Validate and add to PATH +# Validate and add to PATH with precise matching function Setup-Path { $userPath = [Environment]::GetEnvironmentVariable("PATH", "User") @@ -233,7 +337,15 @@ function Setup-Path { return } - if ($userPath -notlike "*$InstallDir*") { + # Use precise PATH entry matching (not substring) + $pathEntries = $userPath -split ';' | ForEach-Object { $_.TrimEnd('\') } + $normalizedInstallDir = $resolvedInstallDir.TrimEnd('\') + + $alreadyInPath = $pathEntries | Where-Object { + $_.Equals($normalizedInstallDir, [System.StringComparison]::OrdinalIgnoreCase) + } + + if (-not $alreadyInPath) { Write-Info "Adding $InstallDir to PATH..." [Environment]::SetEnvironmentVariable("PATH", "$userPath;$InstallDir", "User") $env:PATH = "$env:PATH;$InstallDir" diff --git a/setup.bat b/setup.bat index df65fa8..e5551e3 100644 --- a/setup.bat +++ b/setup.bat @@ -2,6 +2,9 @@ setlocal enabledelayedexpansion REM Quick setup script for Windows REM Run as Administrator: setup.bat +REM +REM SECURITY: This script validates environment before executing +REM to prevent path manipulation attacks. echo === Host UK Developer Workspace Setup === echo. @@ -15,38 +18,82 @@ if !errorlevel! neq 0 ( exit /b 1 ) -REM Install dependencies +REM === SECURITY: Validate LOCALAPPDATA === +REM Ensure LOCALAPPDATA is set and appears to be within user profile +if "%LOCALAPPDATA%"=="" ( + echo ERROR: LOCALAPPDATA environment variable is not set + goto :error +) + +if "%USERPROFILE%"=="" ( + echo ERROR: USERPROFILE environment variable is not set + goto :error +) + +REM Check that LOCALAPPDATA starts with USERPROFILE (basic validation) +REM This prevents redirection attacks where LOCALAPPDATA points elsewhere +echo !LOCALAPPDATA! | findstr /i /b /c:"!USERPROFILE!" >nul +if !errorlevel! neq 0 ( + echo ERROR: LOCALAPPDATA does not appear to be within user profile + echo LOCALAPPDATA: !LOCALAPPDATA! + echo USERPROFILE: !USERPROFILE! + echo This may indicate a path manipulation attack. Aborting. + goto :error +) + +REM Validate paths don't contain suspicious characters +echo !LOCALAPPDATA! | findstr /r "[<>|&^]" >nul +if !errorlevel! equ 0 ( + echo ERROR: LOCALAPPDATA contains invalid characters + goto :error +) + +REM === Install dependencies === echo Installing dependencies... call powershell -ExecutionPolicy Bypass -File "%~dp0scripts\install-deps.ps1" if !errorlevel! neq 0 goto :error -REM Install core CLI +REM === Install core CLI === echo. echo Installing core CLI... call powershell -ExecutionPolicy Bypass -File "%~dp0scripts\install-core.ps1" if !errorlevel! neq 0 goto :error -REM Refresh PATH -set "PATH=%PATH%;%LOCALAPPDATA%\Programs\core" +REM === Validate install path before use === +set "CORE_PATH=!LOCALAPPDATA!\Programs\core" -REM Verify core.exe exists before running -if not exist "%LOCALAPPDATA%\Programs\core\core.exe" ( - echo ERROR: core.exe not found at %LOCALAPPDATA%\Programs\core\core.exe +REM Verify the path exists and is a directory (not a symlink to elsewhere) +if not exist "!CORE_PATH!\core.exe" ( + echo ERROR: core.exe not found at !CORE_PATH!\core.exe goto :error ) -REM Run doctor +REM Check if it's a symlink/junction (basic check via attributes) +for %%F in ("!CORE_PATH!") do ( + set "ATTRS=%%~aF" +) +echo !ATTRS! | findstr /c:"l" >nul +if !errorlevel! equ 0 ( + echo ERROR: Install directory appears to be a symbolic link + echo This may indicate a symlink attack. Aborting. + goto :error +) + +REM Refresh PATH for this session +set "PATH=%PATH%;!CORE_PATH!" + +REM === Run doctor === echo. echo === Verifying environment === -call "%LOCALAPPDATA%\Programs\core\core.exe" doctor +call "!CORE_PATH!\core.exe" doctor if !errorlevel! neq 0 ( echo WARNING: core doctor reported issues ) -REM Clone repos +REM === Clone repos === echo. echo === Cloning repositories === -call "%LOCALAPPDATA%\Programs\core\core.exe" setup +call "!CORE_PATH!\core.exe" setup if !errorlevel! neq 0 goto :error echo. From 7eead7d48aebf3458131f306beca3876516ab058 Mon Sep 17 00:00:00 2001 From: unknown <49066403+bodane@users.noreply.github.com> Date: Sun, 1 Feb 2026 00:40:58 +1100 Subject: [PATCH 06/10] security: expand character filtering and improve junction detection - Add backtick and percent sign to blocked characters - Use fsutil reparsepoint query for reliable symlink/junction detection - Keep attribute check as fallback defense layer Co-Authored-By: Claude Opus 4.5 --- setup.bat | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/setup.bat b/setup.bat index e5551e3..0957006 100644 --- a/setup.bat +++ b/setup.bat @@ -41,10 +41,16 @@ if !errorlevel! neq 0 ( goto :error ) -REM Validate paths don't contain suspicious characters -echo !LOCALAPPDATA! | findstr /r "[<>|&^]" >nul +REM Validate paths don't contain suspicious characters that could enable injection +REM Blocks: < > | & ^ ` %% (shell metacharacters) +echo !LOCALAPPDATA! | findstr /r "[<>|&^`]" >nul if !errorlevel! equ 0 ( - echo ERROR: LOCALAPPDATA contains invalid characters + echo ERROR: LOCALAPPDATA contains invalid shell characters + goto :error +) +echo !LOCALAPPDATA! | findstr /c:"%%" >nul +if !errorlevel! equ 0 ( + echo ERROR: LOCALAPPDATA contains percent signs goto :error ) @@ -68,7 +74,15 @@ if not exist "!CORE_PATH!\core.exe" ( goto :error ) -REM Check if it's a symlink/junction (basic check via attributes) +REM Check if it's a symlink/junction using fsutil (more reliable than attributes) +fsutil reparsepoint query "!CORE_PATH!" >nul 2>&1 +if !errorlevel! equ 0 ( + echo ERROR: Install directory is a reparse point (symlink or junction^) + echo This may indicate a symlink attack. Aborting. + goto :error +) + +REM Fallback: also check attributes for symlink indicator for %%F in ("!CORE_PATH!") do ( set "ATTRS=%%~aF" ) From 1248758d46db1017d8999982c2a510312ba9e9ae Mon Sep 17 00:00:00 2001 From: unknown <49066403+bodane@users.noreply.github.com> Date: Sun, 1 Feb 2026 00:46:46 +1100 Subject: [PATCH 07/10] security: fix single percent detection and add fsutil to PowerShell setup.bat: - Fix percent sign detection to catch single % (not just %%) - Use string substitution for reliable detection install-core.ps1: - Add fsutil reparsepoint query to Test-SecureDirectory - Matches batch script's dual-layer detection approach - Keep .NET attribute check as fallback Co-Authored-By: Claude Opus 4.5 --- scripts/install-core.ps1 | 9 +++++++-- setup.bat | 6 ++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/install-core.ps1 b/scripts/install-core.ps1 index e724d08..62db1ee 100644 --- a/scripts/install-core.ps1 +++ b/scripts/install-core.ps1 @@ -69,9 +69,14 @@ function Test-SecureDirectory { return $true # Directory doesn't exist yet, will be created } - $dirInfo = Get-Item $Path -Force + # Primary check: use fsutil for reliable reparse point detection (matches batch script) + $fsutilResult = & fsutil reparsepoint query $Path 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Err "Directory '$Path' is a reparse point (symlink or junction). Possible symlink attack detected." + } - # Check for symlinks/junctions + # Fallback: check .NET attributes + $dirInfo = Get-Item $Path -Force if ($dirInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { Write-Err "Directory '$Path' is a symbolic link or junction. Possible symlink attack detected." } diff --git a/setup.bat b/setup.bat index 0957006..9aed549 100644 --- a/setup.bat +++ b/setup.bat @@ -48,8 +48,10 @@ if !errorlevel! equ 0 ( echo ERROR: LOCALAPPDATA contains invalid shell characters goto :error ) -echo !LOCALAPPDATA! | findstr /c:"%%" >nul -if !errorlevel! equ 0 ( +REM Check for percent signs (both single and double) +set "TEMP_CHECK=!LOCALAPPDATA!" +set "TEMP_CHECK=!TEMP_CHECK:%%=!" +if not "!TEMP_CHECK!"=="!LOCALAPPDATA!" ( echo ERROR: LOCALAPPDATA contains percent signs goto :error ) From 05bd711219a603f2e92d4647e3efa3436cb0065b Mon Sep 17 00:00:00 2001 From: unknown <49066403+bodane@users.noreply.github.com> Date: Sun, 1 Feb 2026 00:54:45 +1100 Subject: [PATCH 08/10] fix: production quality improvements - Add PowerShell 4.0+ version check at startup - Add disk space check (100MB minimum) before install - Add try/finally cleanup for download temp files (handles Ctrl+C) - Fix PATH duplicate semicolons by trimming before append - Update header with requirements documentation Co-Authored-By: Claude Opus 4.5 --- scripts/install-core.ps1 | 55 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/scripts/install-core.ps1 b/scripts/install-core.ps1 index 62db1ee..a1266a1 100644 --- a/scripts/install-core.ps1 +++ b/scripts/install-core.ps1 @@ -1,6 +1,11 @@ # Install the Core CLI (Windows) # Run: .\scripts\install-core.ps1 # +# REQUIREMENTS: +# - PowerShell 4.0 or later +# - Windows 10/11 or Windows Server 2016+ +# - 100MB free disk space +# # SECURITY CONTROLS: # - Version pinning prevents supply chain attacks via tag manipulation # - SHA256 hash verification ensures binary integrity @@ -15,8 +20,15 @@ $ErrorActionPreference = "Stop" +# Check PowerShell version (4.0+ required for Get-FileHash and other features) +if ($PSVersionTable.PSVersion.Major -lt 4) { + Write-Host "[ERROR] PowerShell 4.0 or later is required. Current version: $($PSVersionTable.PSVersion)" -ForegroundColor Red + exit 1 +} + $Repo = "host-uk/core" $Version = "v0.1.0" # Pinned version - update when releasing new versions +$MinDiskSpaceMB = 100 # Minimum required disk space in MB function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Green } function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow } @@ -26,6 +38,32 @@ function Test-Command($cmd) { return [bool](Get-Command $cmd -ErrorAction SilentlyContinue) } +# Check available disk space +function Test-DiskSpace { + param([string]$Path) + + try { + # Get the drive from the path + $drive = [System.IO.Path]::GetPathRoot($Path) + if ([string]::IsNullOrEmpty($drive)) { + $drive = [System.IO.Path]::GetPathRoot($env:LOCALAPPDATA) + } + + $driveInfo = Get-PSDrive -Name $drive.Substring(0, 1) -ErrorAction Stop + $freeSpaceMB = [math]::Round($driveInfo.Free / 1MB, 2) + + if ($freeSpaceMB -lt $MinDiskSpaceMB) { + Write-Err "Insufficient disk space. Need at least ${MinDiskSpaceMB}MB, but only ${freeSpaceMB}MB available on drive $drive" + } + + Write-Info "Disk space check passed (${freeSpaceMB}MB available)" + return $true + } catch { + Write-Warn "Could not verify disk space: $($_.Exception.Message)" + return $true # Continue anyway if check fails + } +} + # Validate and get secure install directory function Get-SecureInstallDir { # Validate LOCALAPPDATA is within user profile @@ -179,6 +217,9 @@ function Download-Binary { Write-Info "Attempting to download pre-built binary (version $Version)..." Write-Info "URL: $binaryUrl" + # Track temp file for cleanup + $tempExe = $null + try { # Create and verify install directory New-SecureDirectory -Path $InstallDir @@ -204,7 +245,6 @@ function Download-Binary { } if ([string]::IsNullOrEmpty($expectedHash)) { - Remove-Item -Path $tempExe -Force -ErrorAction SilentlyContinue Write-Err "Could not find checksum for core-windows-$arch.exe in checksums.txt" } @@ -217,6 +257,7 @@ function Download-Binary { # Atomic move to final location (same filesystem) $finalPath = Join-Path $InstallDir "core.exe" Move-Item -Path $tempExe -Destination $finalPath -Force + $tempExe = $null # Clear so finally block doesn't try to delete Write-Info "Downloaded and verified: $finalPath" return $true @@ -232,6 +273,11 @@ function Download-Binary { Write-Warn "Download failed: $($_.Exception.Message)" Write-Warn "Will attempt to build from source" return $false + } finally { + # Clean up temp file if it exists (handles Ctrl+C and other interruptions) + if ($tempExe -and (Test-Path $tempExe -ErrorAction SilentlyContinue)) { + Remove-Item -Path $tempExe -Force -ErrorAction SilentlyContinue + } } } @@ -352,7 +398,9 @@ function Setup-Path { if (-not $alreadyInPath) { Write-Info "Adding $InstallDir to PATH..." - [Environment]::SetEnvironmentVariable("PATH", "$userPath;$InstallDir", "User") + # Trim trailing semicolons to prevent duplicates + $cleanPath = $userPath.TrimEnd(';') + [Environment]::SetEnvironmentVariable("PATH", "$cleanPath;$InstallDir", "User") $env:PATH = "$env:PATH;$InstallDir" } } @@ -378,6 +426,9 @@ function Verify { function Main { Write-Info "Installing Core CLI (version $Version)..." + # Check disk space before starting + Test-DiskSpace -Path $InstallDir + # Try download first, fallback to build if (-not (Download-Binary)) { Build-FromSource From d9a8fe279e2cf691d5bd79cbfc4216b6d75a5a44 Mon Sep 17 00:00:00 2001 From: unknown <49066403+bodane@users.noreply.github.com> Date: Sun, 1 Feb 2026 01:19:45 +1100 Subject: [PATCH 09/10] security: harden shell scripts with integrity verification - Add SHA256 checksum verification to install-core.sh binary download - Add version pinning (v0.1.0) to install-core.sh matching PowerShell version - Use mktemp for secure temp files instead of predictable /tmp/core - Add symlink detection to prevent directory traversal attacks - Add GPG signature verification for git tags when available - Add checksum verification for Go binary in install-deps.sh - Add SHA384 signature verification for Composer installer - Replace Invoke-Expression with download-verify-execute in install-deps.ps1 - Download Homebrew/NodeSource scripts to temp files before execution - Add security documentation and warnings for external scripts Co-Authored-By: Claude Opus 4.5 --- scripts/install-core.sh | 230 +++++++++++++++++++++++++++++++++------ scripts/install-deps.ps1 | 26 ++++- scripts/install-deps.sh | 116 ++++++++++++++++++-- 3 files changed, 329 insertions(+), 43 deletions(-) diff --git a/scripts/install-core.sh b/scripts/install-core.sh index 841f6d5..1d38714 100755 --- a/scripts/install-core.sh +++ b/scripts/install-core.sh @@ -1,10 +1,27 @@ #!/usr/bin/env bash set -e -# Install the Core CLI -# Either downloads a pre-built binary or builds from source +# Install the Core CLI (Unix/Linux/macOS) +# Run: ./scripts/install-core.sh +# +# REQUIREMENTS: +# - curl or wget +# - sha256sum (Linux) or shasum (macOS) +# - git and go (for building from source) +# +# SECURITY CONTROLS: +# - Version pinning prevents supply chain attacks via tag manipulation +# - SHA256 hash verification ensures binary integrity +# - Secure temp file creation prevents symlink attacks +# - Symlink detection prevents directory traversal attacks +# - GPG signature verification (when available) ensures code authenticity +# +# KNOWN LIMITATIONS: +# - Checksums are fetched from same origin as binaries (consider separate trust root) +# - No TLS certificate pinning (relies on system CA store) -REPO="Snider/Core" +REPO="host-uk/core" +VERSION="v0.1.0" # Pinned version - update when releasing new versions INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" BUILD_FROM_SOURCE="${BUILD_FROM_SOURCE:-auto}" @@ -38,49 +55,195 @@ detect_os() { esac } -# Try to download pre-built binary -download_binary() { - local os=$(detect_os) - local arch=$(detect_arch) - local url="https://github.com/$REPO/releases/latest/download/core-${os}-${arch}" - - if [[ "$os" == "windows" ]]; then - url="${url}.exe" - fi - - info "Attempting to download pre-built binary..." - info "URL: $url" - - if curl -fsSL -o /tmp/core "$url" 2>/dev/null; then - chmod +x /tmp/core - mkdir -p "$INSTALL_DIR" - mv /tmp/core "$INSTALL_DIR/core" - info "Downloaded to $INSTALL_DIR/core" - return 0 +# Compute SHA256 hash (cross-platform) +compute_sha256() { + local file=$1 + if has sha256sum; then + sha256sum "$file" | cut -d' ' -f1 + elif has shasum; then + shasum -a 256 "$file" | cut -d' ' -f1 else - warn "No pre-built binary available, will build from source" - return 1 + error "No SHA256 tool available (need sha256sum or shasum)" fi } -# Build from source +# Verify SHA256 hash of downloaded file +verify_hash() { + local file=$1 + local expected_hash=$2 + local actual_hash + + actual_hash=$(compute_sha256 "$file") + + if [[ "${actual_hash,,}" != "${expected_hash,,}" ]]; then + rm -f "$file" + error "Hash verification failed! Expected: $expected_hash, Got: $actual_hash. The downloaded file may be corrupted or tampered with." + fi + + info "Hash verification passed (SHA256: ${actual_hash:0:16}...)" +} + +# Check if directory is a symlink (security check) +check_not_symlink() { + local path=$1 + + if [[ -L "$path" ]]; then + error "Directory '$path' is a symbolic link. Possible symlink attack detected." + fi + + # Additional check using file type + if [[ -e "$path" ]] && [[ ! -d "$path" ]]; then + error "Path '$path' exists but is not a directory." + fi +} + +# Try to download pre-built binary with integrity verification +download_binary() { + local os=$(detect_os) + local arch=$(detect_arch) + local binary_name="core-${os}-${arch}" + local binary_url="https://github.com/$REPO/releases/download/$VERSION/${binary_name}" + local checksum_url="https://github.com/$REPO/releases/download/$VERSION/checksums.txt" + + if [[ "$os" == "windows" ]]; then + binary_name="${binary_name}.exe" + binary_url="${binary_url}.exe" + fi + + info "Attempting to download pre-built binary (version $VERSION)..." + info "URL: $binary_url" + + # Use secure temp file (prevents symlink attacks) + local temp_file + temp_file=$(mktemp "${TMPDIR:-/tmp}/core.XXXXXXXXXX") || error "Failed to create temp file" + + # Ensure cleanup on exit/interrupt + trap "rm -f '$temp_file'" EXIT INT TERM + + # Download binary + if ! curl -fsSL -o "$temp_file" "$binary_url" 2>/dev/null; then + rm -f "$temp_file" + trap - EXIT INT TERM + warn "No pre-built binary available, will build from source" + return 1 + fi + + # Download and verify checksum + info "Verifying download integrity..." + local checksums + checksums=$(curl -fsSL "$checksum_url" 2>/dev/null) || { + rm -f "$temp_file" + trap - EXIT INT TERM + warn "Could not download checksums, will build from source" + return 1 + } + + # Parse checksum file (format: "hash filename") + local expected_hash + expected_hash=$(echo "$checksums" | grep -E "^[a-fA-F0-9]{64}\s+.*${binary_name}$" | head -1 | cut -d' ' -f1) + + if [[ -z "$expected_hash" ]]; then + rm -f "$temp_file" + trap - EXIT INT TERM + error "Could not find checksum for $binary_name in checksums.txt" + fi + + # Verify hash BEFORE any move operation + verify_hash "$temp_file" "$expected_hash" + + # Verify install directory is safe + check_not_symlink "$INSTALL_DIR" + mkdir -p "$INSTALL_DIR" + check_not_symlink "$INSTALL_DIR" # Re-verify after mkdir + + # Make executable and move to final location + chmod +x "$temp_file" + mv "$temp_file" "$INSTALL_DIR/core" + + trap - EXIT INT TERM # Clear trap since file was moved + + info "Downloaded and verified: $INSTALL_DIR/core" + return 0 +} + +# Verify GPG signature on git tag (if gpg is available) +verify_git_tag_signature() { + local repo_path=$1 + local tag=$2 + + if ! has gpg; then + warn "GPG not available - skipping tag signature verification" + warn "For enhanced security, install GPG and import the project signing key" + return 0 # Continue without verification + fi + + pushd "$repo_path" > /dev/null + local result + result=$(git tag -v "$tag" 2>&1) || { + # Check if tag is unsigned vs signature invalid + if echo "$result" | grep -q "error: no signature found"; then + warn "Tag $tag is not signed - continuing without signature verification" + popd > /dev/null + return 0 + else + popd > /dev/null + error "GPG signature verification FAILED for tag $tag. Possible tampering detected." + fi + } + info "GPG signature verified for tag $tag" + popd > /dev/null +} + +# Build from source with security checks build_from_source() { + if ! has git; then + error "Git is required to build from source. Run './scripts/install-deps.sh' first" + fi + if ! has go; then error "Go is required to build from source. Run './scripts/install-deps.sh' first" fi - local tmpdir=$(mktemp -d) - info "Cloning $REPO..." - git clone --depth 1 "https://github.com/$REPO.git" "$tmpdir/Core" + # Create secure temp directory + local tmpdir + tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/core-build.XXXXXXXXXX") || error "Failed to create temp directory" + + # Set restrictive permissions on temp directory + chmod 700 "$tmpdir" + + # Ensure cleanup on exit/interrupt + trap "rm -rf '$tmpdir'" EXIT INT TERM + + info "Cloning $REPO (version $VERSION)..." + local clone_dir="$tmpdir/Core" + + # Clone specific tag for reproducibility + if ! git clone --depth 1 --branch "$VERSION" "https://github.com/$REPO.git" "$clone_dir"; then + error "Failed to clone repository at version $VERSION" + fi + + # Verify GPG signature on tag (if available) + verify_git_tag_signature "$clone_dir" "$VERSION" info "Building core CLI..." - cd "$tmpdir/Core" - go build -o core ./cmd/core + pushd "$clone_dir" > /dev/null + if ! go build -o core ./cmd/core; then + popd > /dev/null + error "Go build failed" + fi + popd > /dev/null + # Verify install directory is safe + check_not_symlink "$INSTALL_DIR" mkdir -p "$INSTALL_DIR" - mv core "$INSTALL_DIR/core" + check_not_symlink "$INSTALL_DIR" # Re-verify after mkdir + # Move built binary to install location + mv "$clone_dir/core" "$INSTALL_DIR/core" + + trap - EXIT INT TERM # Clear trap rm -rf "$tmpdir" + info "Built and installed to $INSTALL_DIR/core" } @@ -118,9 +281,12 @@ verify() { } main() { - info "Installing Core CLI..." + info "Installing Core CLI (version $VERSION)..." + # Verify install directory is safe before starting + check_not_symlink "$INSTALL_DIR" mkdir -p "$INSTALL_DIR" + check_not_symlink "$INSTALL_DIR" # Try download first, fallback to build if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then diff --git a/scripts/install-deps.ps1 b/scripts/install-deps.ps1 index f3ec533..e9c70b3 100644 --- a/scripts/install-deps.ps1 +++ b/scripts/install-deps.ps1 @@ -1,5 +1,10 @@ # Install system dependencies for Host UK development (Windows) # Run: .\scripts\install-deps.ps1 +# +# SECURITY NOTES: +# - Chocolatey installer is downloaded to temp file before execution +# - HTTPS is enforced for all downloads +# - For high-security environments, consider auditing install scripts $ErrorActionPreference = "Stop" @@ -12,6 +17,8 @@ function Test-Command($cmd) { } # Install Chocolatey if not present +# NOTE: Chocolatey's install script changes frequently, making checksum verification impractical. +# The script is fetched over HTTPS. For high-security environments, audit the script first. function Install-Chocolatey { if (Test-Command choco) { Write-Info "Chocolatey already installed" @@ -19,9 +26,26 @@ function Install-Chocolatey { } Write-Info "Installing Chocolatey..." + Write-Warn "This downloads and executes a script from chocolatey.org. Review at: https://community.chocolatey.org/install.ps1" + Set-ExecutionPolicy Bypass -Scope Process -Force [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 - Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + + # Download to temp file first (allows manual inspection if needed, avoids Invoke-Expression with direct download) + $tempScript = Join-Path ([System.IO.Path]::GetTempPath()) "choco-install.$([System.Guid]::NewGuid().ToString('N').Substring(0,8)).ps1" + + try { + Write-Info "Downloading Chocolatey installer..." + Invoke-WebRequest -Uri 'https://community.chocolatey.org/install.ps1' -OutFile $tempScript -UseBasicParsing + + Write-Info "Executing Chocolatey installer..." + & $tempScript + } finally { + # Clean up temp file + if (Test-Path $tempScript) { + Remove-Item -Path $tempScript -Force -ErrorAction SilentlyContinue + } + } # Refresh PATH $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User") diff --git a/scripts/install-deps.sh b/scripts/install-deps.sh index 572e7ac..387aa36 100755 --- a/scripts/install-deps.sh +++ b/scripts/install-deps.sh @@ -3,16 +3,55 @@ set -e # Install system dependencies for Host UK development # Supports: macOS (brew), Linux (apt/dnf), Windows (choco via WSL) +# +# SECURITY NOTES: +# - External install scripts (Homebrew, NodeSource) are downloaded over HTTPS +# - Go binary is verified via SHA256 checksum +# - Composer installer is verified via SHA256 checksum +# - Consider auditing external scripts before running in sensitive environments RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' +# Pinned versions and checksums for security +GO_VERSION="1.22.0" +GO_AMD64_SHA256="f6c8a87aa03b92c4b0bf3d558e28ea03006eb29db78917daec5cfb6ec1046265" +COMPOSER_EXPECTED_SIG="dac665fdc30fdd8ec78b38b9800061b4150413ff2e3b6f88543c636f7cd84f6db9189d43a81e5503cda447da73c7e5b6" + info() { echo -e "${GREEN}[INFO]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } +# Compute SHA256 hash (cross-platform) +compute_sha256() { + local file=$1 + if command -v sha256sum &> /dev/null; then + sha256sum "$file" | cut -d' ' -f1 + elif command -v shasum &> /dev/null; then + shasum -a 256 "$file" | cut -d' ' -f1 + else + error "No SHA256 tool available (need sha256sum or shasum)" + fi +} + +# Verify SHA256 hash of downloaded file +verify_hash() { + local file=$1 + local expected_hash=$2 + local actual_hash + + actual_hash=$(compute_sha256 "$file") + + if [[ "${actual_hash,,}" != "${expected_hash,,}" ]]; then + rm -f "$file" + error "Hash verification failed! Expected: $expected_hash, Got: $actual_hash" + fi + + info "Hash verification passed" +} + # Detect OS detect_os() { case "$(uname -s)" in @@ -29,6 +68,9 @@ has() { } # Install Homebrew (macOS/Linux) +# NOTE: Homebrew's install script changes frequently, making checksum verification impractical. +# The script is fetched over HTTPS from GitHub. For high-security environments, +# consider auditing the script manually before running. install_brew() { if has brew; then info "Homebrew already installed" @@ -36,7 +78,18 @@ install_brew() { fi info "Installing Homebrew..." - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + warn "This downloads and executes a script from GitHub. Review at: https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" + + # Download to temp file first (allows manual inspection if needed) + local temp_script + temp_script=$(mktemp "${TMPDIR:-/tmp}/brew-install.XXXXXXXXXX.sh") + curl -fsSL -o "$temp_script" https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh || { + rm -f "$temp_script" + error "Failed to download Homebrew installer" + } + + /bin/bash "$temp_script" + rm -f "$temp_script" # Add to PATH for this session if [[ -f /opt/homebrew/bin/brew ]]; then @@ -115,11 +168,24 @@ setup_linux_apt() { if ! has go; then info "Installing Go..." sudo apt-get install -y golang-go || { - # Fallback to manual install for newer version - curl -LO https://go.dev/dl/go1.22.0.linux-amd64.tar.gz + # Fallback to manual install for newer version with integrity verification + local go_tarball="go${GO_VERSION}.linux-amd64.tar.gz" + local go_url="https://go.dev/dl/${go_tarball}" + local temp_file + temp_file=$(mktemp "${TMPDIR:-/tmp}/go.XXXXXXXXXX.tar.gz") + + info "Downloading Go $GO_VERSION..." + curl -fsSL -o "$temp_file" "$go_url" || { + rm -f "$temp_file" + error "Failed to download Go" + } + + info "Verifying Go download integrity..." + verify_hash "$temp_file" "$GO_AMD64_SHA256" + sudo rm -rf /usr/local/go - sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz - rm go1.22.0.linux-amd64.tar.gz + sudo tar -C /usr/local -xzf "$temp_file" + rm -f "$temp_file" export PATH=$PATH:/usr/local/go/bin } fi @@ -127,17 +193,47 @@ setup_linux_apt() { # PHP apt_install php - # Composer + # Composer (with installer signature verification) if ! has composer; then info "Installing Composer..." - curl -sS https://getcomposer.org/installer | php - sudo mv composer.phar /usr/local/bin/composer + local temp_dir + temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/composer.XXXXXXXXXX") + chmod 700 "$temp_dir" + + # Download installer + curl -fsSL -o "$temp_dir/composer-setup.php" https://getcomposer.org/installer + + # Verify installer signature (SHA384) + local actual_sig + actual_sig=$(php -r "echo hash_file('sha384', '$temp_dir/composer-setup.php');") + if [[ "$actual_sig" != "$COMPOSER_EXPECTED_SIG" ]]; then + rm -rf "$temp_dir" + error "Composer installer signature verification failed!" + fi + info "Composer installer signature verified" + + # Run installer + php "$temp_dir/composer-setup.php" --install-dir="$temp_dir" + sudo mv "$temp_dir/composer.phar" /usr/local/bin/composer + rm -rf "$temp_dir" fi - # Node + # Node (via NodeSource) + # NOTE: NodeSource setup script changes frequently, making checksum verification impractical. + # For high-security environments, consider using nvm or building from source. if ! has node; then info "Installing Node.js..." - curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + warn "This downloads and executes a script from NodeSource. Review at: https://deb.nodesource.com/setup_20.x" + + local temp_script + temp_script=$(mktemp "${TMPDIR:-/tmp}/nodesource-setup.XXXXXXXXXX.sh") + curl -fsSL -o "$temp_script" https://deb.nodesource.com/setup_20.x || { + rm -f "$temp_script" + error "Failed to download NodeSource setup script" + } + + sudo -E bash "$temp_script" + rm -f "$temp_script" sudo apt-get install -y nodejs fi From 15778b7a5fe8c3385e6bded6e0a4a07171b382a4 Mon Sep 17 00:00:00 2001 From: unknown <49066403+bodane@users.noreply.github.com> Date: Sun, 1 Feb 2026 02:00:26 +1100 Subject: [PATCH 10/10] fix: address CodeRabbit review feedback - Separate local declaration from assignment to avoid masking errors (SC2155) - Add exit code check after Homebrew installer execution - Add exit code check after NodeSource setup script execution - Add LASTEXITCODE check after Chocolatey installer execution Co-Authored-By: Claude Opus 4.5 --- scripts/install-core.sh | 11 ++++++----- scripts/install-deps.ps1 | 3 +++ scripts/install-deps.sh | 10 ++++++++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/scripts/install-core.sh b/scripts/install-core.sh index 1d38714..8c1f23f 100755 --- a/scripts/install-core.sh +++ b/scripts/install-core.sh @@ -99,11 +99,12 @@ check_not_symlink() { # Try to download pre-built binary with integrity verification download_binary() { - local os=$(detect_os) - local arch=$(detect_arch) - local binary_name="core-${os}-${arch}" - local binary_url="https://github.com/$REPO/releases/download/$VERSION/${binary_name}" - local checksum_url="https://github.com/$REPO/releases/download/$VERSION/checksums.txt" + local os arch binary_name binary_url checksum_url + os=$(detect_os) + arch=$(detect_arch) + binary_name="core-${os}-${arch}" + binary_url="https://github.com/$REPO/releases/download/$VERSION/${binary_name}" + checksum_url="https://github.com/$REPO/releases/download/$VERSION/checksums.txt" if [[ "$os" == "windows" ]]; then binary_name="${binary_name}.exe" diff --git a/scripts/install-deps.ps1 b/scripts/install-deps.ps1 index e9c70b3..e3573c6 100644 --- a/scripts/install-deps.ps1 +++ b/scripts/install-deps.ps1 @@ -40,6 +40,9 @@ function Install-Chocolatey { Write-Info "Executing Chocolatey installer..." & $tempScript + if ($LASTEXITCODE -ne 0) { + Write-Err "Chocolatey installation failed with exit code $LASTEXITCODE" + } } finally { # Clean up temp file if (Test-Path $tempScript) { diff --git a/scripts/install-deps.sh b/scripts/install-deps.sh index 387aa36..39c2412 100755 --- a/scripts/install-deps.sh +++ b/scripts/install-deps.sh @@ -88,7 +88,10 @@ install_brew() { error "Failed to download Homebrew installer" } - /bin/bash "$temp_script" + /bin/bash "$temp_script" || { + rm -f "$temp_script" + error "Homebrew installation failed" + } rm -f "$temp_script" # Add to PATH for this session @@ -232,7 +235,10 @@ setup_linux_apt() { error "Failed to download NodeSource setup script" } - sudo -E bash "$temp_script" + sudo -E bash "$temp_script" || { + rm -f "$temp_script" + error "NodeSource setup failed" + } rm -f "$temp_script" sudo apt-get install -y nodejs fi