Skip to content

Harden PowerShell execution, package source policy, and privileged file handling#1

Open
Copilot wants to merge 6 commits into
masterfrom
copilot/run-security-review
Open

Harden PowerShell execution, package source policy, and privileged file handling#1
Copilot wants to merge 6 commits into
masterfrom
copilot/run-security-review

Conversation

Copilot AI commented Apr 27, 2026

Copy link
Copy Markdown

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

    • Route PowerShell, Chocolatey, and Winget invocations through trusted executable resolution instead of PATH/current-directory lookup.
    • Replace wrapper/menu relaunches that used blanket ExecutionPolicy Bypass with direct script execution or trusted relaunch helpers.
    • Constrain elevated script launches to .ps1 files under the repository root.
  • Safer Chocolatey bootstrap

    • Remove the direct DownloadString(...) | iex install flow.
    • Bootstrap Chocolatey by downloading the package locally, showing the SHA256 hash, extracting the package, and requiring explicit confirmation before running the local installer.
    • Keep bootstrap artifacts in a controlled per-user location and clean them up after execution.
  • Package source policy

    • Default Chocolatey installs/searches/upgrades to the approved community API source.
    • Default Winget installs/searches/info to the approved winget source.
    • Add stronger confirmation prompts for install/update/sync/remove flows, especially bulk actions.
  • Managed file-path restrictions

    • Restrict package list reads/writes to the repository data/ directory.
    • Normalize and validate managed paths before use to prevent elevated writes to arbitrary locations.
  • Log exposure reduction

    • Keep console logging intact, but persist only warning/error entries by default.
    • Allow broader log persistence via CHOCO_MANAGER_LOG_FILE_LEVELS when explicitly needed.
    • Update audit-log UX and README guidance to reflect the new default.
  • Workflow updates

    • Apply the hardened helpers consistently across:
      • scripts/main-menu.ps1
      • src/Core/core-functions.ps1
      • src/Choco/*.ps1
      • src/Winget/winget-utils.ps1

Example of the new command pattern:

Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @(
    "install",
    $safeName,
    "-y",
    "--source",
    (Get-TrustedChocolateySource)
)

@arrfour arrfour marked this pull request as ready for review April 27, 2026 20:17
Copilot AI review requested due to automatic review settings April 27, 2026 20:17
@arrfour

arrfour commented Apr 27, 2026

Copy link
Copy Markdown
Owner

let me run this branch in my test environment prior to approval.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 blanket ExecutionPolicy Bypass usage.

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.

Comment thread choco-sync.ps1
@@ -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

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
& (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

Copilot uses AI. Check for mistakes.
Comment thread choco-pack-install.ps1
@@ -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

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
& (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

Copilot uses AI. Check for mistakes.
Comment on lines +427 to +431
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)

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +380 to +383
$packageUri = [Uri]$PackageUrl
if ($packageUri.Scheme -ne "https" -or $packageUri.Host -ne "community.chocolatey.org") {
throw "Rejected Chocolatey package URL: $PackageUrl"
}

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

$commandPath = Get-NormalizedPath -Path $command.Source
$allowedRoots = @(
$env:ChocolateyInstall,

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$env:ChocolateyInstall,

Copilot uses AI. Check for mistakes.
Comment thread list-choco-apps.ps1
@@ -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

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
& (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

Copilot uses AI. Check for mistakes.
Comment thread choco-utils.ps1
@@ -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

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
& (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

Copilot uses AI. Check for mistakes.
@@ -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

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
& (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

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants