Harden PowerShell execution, package source policy, and privileged file handling#1
Harden PowerShell execution, package source policy, and privileged file handling#1Copilot wants to merge 6 commits into
Conversation
Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com>
Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com>
Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com>
Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com>
Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com>
Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com>
|
let me run this branch in my test environment prior to approval. |
There was a problem hiding this comment.
Pull request overview
This PR hardens the project’s execution and package-management flows by routing Chocolatey/Winget/PowerShell through trusted-path resolution, enforcing approved package sources, restricting managed file paths, and reducing default log persistence.
Changes:
- Introduces trusted executable/script resolution helpers (trusted PowerShell path, allowlisted choco/winget locations, repo-root script constraints) and defaults file log persistence to WARN/ERROR.
- Updates Choco/Winget workflows to use trusted invocations with explicit approved sources plus stronger confirmation prompts.
- Restricts package list read/write paths to the repository
data/directory and updates wrappers/menus to avoid blanketExecutionPolicy Bypassusage.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
src/Core/core-functions.ps1 |
Adds trusted executable/script resolution, managed-path validation, secured Chocolatey bootstrap flow, and log persistence filtering. |
scripts/choco-manager.ps1 |
Removes forced process-scope execution policy bypass/unblock when loading core functions. |
scripts/main-menu.ps1 |
Routes choco/winget calls through trusted helpers and uses Invoke-ScriptFile for launching sub-tools. |
src/Choco/choco-pack-install.ps1 |
Uses trusted choco execution, approved source, managed input path restriction, and confirmation before bulk installs. |
src/Choco/choco-sync.ps1 |
Uses managed input path restriction, trusted choco execution, approved source for installs, and stronger confirmations. |
src/Choco/choco-utils.ps1 |
Uses managed input path restriction, trusted choco execution, approved source, and confirmation for bulk upgrades. |
src/Choco/choco-upgrade-interactive.ps1 |
Uses trusted choco execution and adds confirmation before upgrading all outdated packages. |
src/Choco/list-choco-apps.ps1 |
Defaults output to managed data/ path, validates it, and uses trusted choco execution. |
src/Choco/choco-package-explorer.ps1 |
Routes choco/winget listing/search/info/install/uninstall via trusted execution and approved sources with confirmations. |
src/Winget/winget-utils.ps1 |
Uses trusted winget resolution/execution, enforces approved winget source, and adds confirmation for install/remove. |
README.md |
Documents the new execution policy stance, approved sources, managed data path restriction, bootstrap flow, and new log defaults. |
choco-pack-install.ps1 |
Wrapper updated to directly invoke the underlying script. |
choco-sync.ps1 |
Wrapper updated to directly invoke the underlying script. |
choco-upgrade-interactive.ps1 |
Wrapper updated to directly invoke the underlying script. |
choco-utils.ps1 |
Wrapper updated to directly invoke the underlying script. |
list-choco-apps.ps1 |
Wrapper updated to directly invoke the underlying script. |
choco-package-explorer.ps1 |
Wrapper updated to directly invoke the underlying script. |
winget-utils.ps1 |
Wrapper updated to directly invoke the underlying script. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -1,2 +1,2 @@ | |||
| # Wrapper script for choco-sync | |||
| powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-sync.ps1") @args | |||
| & (Join-Path $PSScriptRoot "src\Choco\choco-sync.ps1") @args | |||
There was a problem hiding this comment.
This wrapper now runs src/Choco/choco-sync.ps1 via &, which executes it in the current PowerShell process. The target script uses exit after requesting elevation, so running the wrapper from an existing PowerShell session will unexpectedly close that session. Consider starting a new PowerShell process (no execution policy bypass required) or updating the target script to avoid exit.
| & (Join-Path $PSScriptRoot "src\Choco\choco-sync.ps1") @args | |
| $scriptPath = Join-Path $PSScriptRoot "src\Choco\choco-sync.ps1" | |
| $powerShellPath = (Get-Process -Id $PID).Path | |
| $argumentList = @('-NoProfile', '-File', $scriptPath) + $args | |
| $process = Start-Process -FilePath $powerShellPath -ArgumentList $argumentList -Wait -PassThru | |
| [Environment]::ExitCode = $process.ExitCode |
| @@ -1,2 +1,2 @@ | |||
| # Wrapper script for choco-pack-install | |||
| powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-pack-install.ps1") @args | |||
| & (Join-Path $PSScriptRoot "src\Choco\choco-pack-install.ps1") @args | |||
There was a problem hiding this comment.
The wrapper now calls src/Choco/choco-pack-install.ps1 in-process. That script contains several exit statements (e.g., after elevation relaunch/cancel), which will terminate the caller’s entire PowerShell session if this wrapper is run from an interactive prompt. Prefer spawning a separate PowerShell process (without forcing ExecutionPolicy Bypass) or refactoring the target script to use return/exceptions.
| & (Join-Path $PSScriptRoot "src\Choco\choco-pack-install.ps1") @args | |
| $scriptPath = Join-Path $PSScriptRoot "src\Choco\choco-pack-install.ps1" | |
| $powerShellExe = (Get-Process -Id $PID).Path | |
| $argumentList = @('-File', $scriptPath) + $args | |
| $process = Start-Process -FilePath $powerShellExe -ArgumentList $argumentList -Wait -PassThru | |
| exit $process.ExitCode |
| Set-Content -Path $bootstrapPath -Value $bootstrapScript -Encoding UTF8 | ||
|
|
||
| try { | ||
| $trustedPowerShell = Get-TrustedPowerShellPath | ||
| Invoke-ElevatedProcess -FilePath $trustedPowerShell -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $bootstrapPath, "-PackageUrl", "https://community.chocolatey.org/api/v2/package/chocolatey", "-SelfPath", $bootstrapPath) |
There was a problem hiding this comment.
Chocolatey bootstrap writes a temporary .ps1 to a user-writable directory and then launches it elevated. There’s a TOCTOU risk: another process running as the same user could modify $bootstrapPath between Set-Content and the UAC prompt being accepted, resulting in unintended elevated code execution. Consider avoiding an on-disk script (e.g., use an encoded/inline command) or pass an expected hash to the elevated process and have it verify its own file contents before executing.
| Set-Content -Path $bootstrapPath -Value $bootstrapScript -Encoding UTF8 | |
| try { | |
| $trustedPowerShell = Get-TrustedPowerShellPath | |
| Invoke-ElevatedProcess -FilePath $trustedPowerShell -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $bootstrapPath, "-PackageUrl", "https://community.chocolatey.org/api/v2/package/chocolatey", "-SelfPath", $bootstrapPath) | |
| $encodedBootstrapScript = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($bootstrapScript)) | |
| try { | |
| $trustedPowerShell = Get-TrustedPowerShellPath | |
| Invoke-ElevatedProcess -FilePath $trustedPowerShell -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-EncodedCommand", $encodedBootstrapScript, "-PackageUrl", "https://community.chocolatey.org/api/v2/package/chocolatey", "-SelfPath", $bootstrapPath) |
| $packageUri = [Uri]$PackageUrl | ||
| if ($packageUri.Scheme -ne "https" -or $packageUri.Host -ne "community.chocolatey.org") { | ||
| throw "Rejected Chocolatey package URL: $PackageUrl" | ||
| } |
There was a problem hiding this comment.
The bootstrap script only validates the initial URL host (community.chocolatey.org) before calling Invoke-WebRequest. Since Invoke-WebRequest follows redirects by default, this doesn’t guarantee the final download origin remains on the approved host. Consider disabling redirects or validating the final response URI host before trusting the downloaded package.
|
|
||
| $commandPath = Get-NormalizedPath -Path $command.Source | ||
| $allowedRoots = @( | ||
| $env:ChocolateyInstall, |
There was a problem hiding this comment.
Resolve-TrustedCommandPath treats $env:ChocolateyInstall as a trusted root for choco.exe. Environment variables are process/user-controlled, so an attacker could set ChocolateyInstall to an untrusted directory and (combined with PATH hijacking) bypass the directory allowlist check. Consider removing $env:ChocolateyInstall from the trusted roots or deriving the install root from a machine-trusted source (e.g., known ProgramData/ProgramFiles locations or registry) before accepting it.
| $env:ChocolateyInstall, |
| @@ -1,2 +1,2 @@ | |||
| # Wrapper script for list-choco-apps | |||
| powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\list-choco-apps.ps1") @args | |||
| & (Join-Path $PSScriptRoot "src\Choco\list-choco-apps.ps1") @args | |||
There was a problem hiding this comment.
The wrapper now invokes the underlying script in-process via & ... @args. The target script (src/Choco/list-choco-apps.ps1) calls exit on failure, which will terminate the caller’s entire PowerShell session when this wrapper is run interactively. Consider launching a separate PowerShell process (without ExecutionPolicy Bypass) or refactoring the target script to use return/exceptions instead of exit.
| & (Join-Path $PSScriptRoot "src\Choco\list-choco-apps.ps1") @args | |
| $targetScript = Join-Path $PSScriptRoot "src\Choco\list-choco-apps.ps1" | |
| $powerShellCommand = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" } | |
| & $powerShellCommand -NoProfile -File $targetScript @args |
| @@ -1,2 +1,2 @@ | |||
| # Wrapper script for choco-utils | |||
| powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-utils.ps1") @args | |||
| & (Join-Path $PSScriptRoot "src\Choco\choco-utils.ps1") @args | |||
There was a problem hiding this comment.
This wrapper now runs src/Choco/choco-utils.ps1 in the current PowerShell process. That script uses exit after relaunching for elevation, which will close the caller’s session if the wrapper is run from an interactive prompt. Prefer spawning a new PowerShell process (no policy bypass needed) or replacing exit in the target script with return/exceptions.
| & (Join-Path $PSScriptRoot "src\Choco\choco-utils.ps1") @args | |
| $targetScript = Join-Path $PSScriptRoot "src\Choco\choco-utils.ps1" | |
| $powerShellExe = if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh' } else { 'powershell' } | |
| & $powerShellExe -NoProfile -File $targetScript @args |
| @@ -1,2 +1,2 @@ | |||
| # Wrapper script for choco-upgrade-interactive | |||
| powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-upgrade-interactive.ps1") @args | |||
| & (Join-Path $PSScriptRoot "src\Choco\choco-upgrade-interactive.ps1") @args | |||
There was a problem hiding this comment.
The wrapper now executes src/Choco/choco-upgrade-interactive.ps1 in-process. That target script calls exit when it relaunches for elevation, which will terminate the user’s current PowerShell session if they run this wrapper interactively. Recommend launching a separate PowerShell process (without blanket policy bypass) or changing the target script to return instead of exit.
| & (Join-Path $PSScriptRoot "src\Choco\choco-upgrade-interactive.ps1") @args | |
| $targetScript = Join-Path $PSScriptRoot "src\Choco\choco-upgrade-interactive.ps1" | |
| $powerShellExe = if ($PSVersionTable.PSEdition -eq 'Core') { | |
| Join-Path $PSHOME 'pwsh.exe' | |
| } else { | |
| Join-Path $PSHOME 'powershell.exe' | |
| } | |
| & $powerShellExe -NoProfile -File $targetScript @args |
This changeset hardens the project’s highest-risk execution paths: privileged PowerShell relaunch, Chocolatey bootstrap, package acquisition, and elevated file access. It replaces implicit trust of PATH, remote bootstrap code, and arbitrary list paths with explicit trust boundaries and safer defaults.
Execution hardening
ExecutionPolicy Bypasswith direct script execution or trusted relaunch helpers..ps1files under the repository root.Safer Chocolatey bootstrap
DownloadString(...) | iexinstall flow.Package source policy
wingetsource.Managed file-path restrictions
data/directory.Log exposure reduction
CHOCO_MANAGER_LOG_FILE_LEVELSwhen explicitly needed.Workflow updates
scripts/main-menu.ps1src/Core/core-functions.ps1src/Choco/*.ps1src/Winget/winget-utils.ps1Example of the new command pattern: