Skip to content

fix: resolve winget to its trusted WindowsApps path to close a binary-planting LPE#1434

Merged
laurentiu021 merged 1 commit into
mainfrom
fix/winget-path-lpe
Jul 11, 2026
Merged

fix: resolve winget to its trusted WindowsApps path to close a binary-planting LPE#1434
laurentiu021 merged 1 commit into
mainfrom
fix/winget-path-lpe

Conversation

@laurentiu021

Copy link
Copy Markdown
Owner

Problem (P1 — local privilege escalation)

Every winget operation (Bulk Installer install/list/search, App Updates upgrade, Uninstaller list) routes through PowerShellRunner.RunProcessAsync("winget", …), which hardens the launch target via SystemPaths.ResolveSystemTool. But winget is not a System32 tool — it is an MSIX app whose real binary lives under %ProgramFiles%\WindowsApps\Microsoft.DesktopAppInstaller_*__8wekyb3d8bbwe\winget.exe, and whose only PATH entry is a per-user execution alias in the user-writable %LOCALAPPDATA%\Microsoft\WindowsApps.

ResolveSystemTool probes only System32 (and the Windows PowerShell folder), so both probes missed and it returned the unrooted bare string "winget". Launched with UseShellExecute=false, Win32 CreateProcess searches the calling process's own load directory first — so a winget.exe planted next to SysManager's portable .exe (commonly run from a user-writable Downloads folder, sometimes elevated via the prominent "Run as administrator" buttons on the winget tabs) would execute with the app's privileges.

The WorkingDirectory=System32 pin the code relied on does not cover this vector: the working directory is CreateProcess search item #2, the application load directory is item #1. This is the unclosed sibling of the System32 LPE fixed for pinned tools in v1.52.91 / v1.52.92.

Fix

Adds SystemPaths.ResolveWinget():

  • Enumerates %ProgramFiles%\WindowsApps\Microsoft.DesktopAppInstaller_*__8wekyb3d8bbwe (the fixed publisher hash is the trust anchor; that folder is writable by administrators / TrustedInstaller only).
  • Picks the highest package version (parsed as System.Version, so 1.29 > 1.9) that actually contains winget.exe (the _neutral_~_ resources package doesn't).
  • Fails closed: if no trusted install is found, returns a rooted System32\winget.exe path (normally non-existent, surfacing the same Win32Exception the winget callers already handle) rather than the plantable bare name.
  • The per-user %LOCALAPPDATA% alias is deliberately never used for the (potentially elevated) launch.

ResolveSystemTool now delegates winget / winget.exe to ResolveWinget(). All 7 winget call sites across 3 services flow through the single RunProcessAsync seam, so the class is fixed in one place. Two stale code comments that credited the WorkingDirectory pin with closing this vector are corrected.

Tests

New SystemPathsTests cases:

  • winget / winget.exe (and case variants) never return the bare name and are always rooted.
  • ResolveWinget never points under user-writable LocalAppData.
  • When App Installer is present, resolves under WindowsApps to an existing winget.exe.
  • ParsePackageVersion orders numerically (1.29 > 1.9) and returns 0.0 (sorts last, never throws) for unexpected shapes.

Runtime-verified via a temp .NET 10 harness (banned dotnet test not used locally): ResolveWinget() → the real trusted path …\Microsoft.DesktopAppInstaller_1.29.279.0_x64__8wekyb3d8bbwe\winget.exe, rooted + exists + not under LocalAppData; launching winget from that path succeeds (--version → v1.29.280). Red→green: pre-fix ResolveSystemTool("winget") returned bare "winget" (test's NotEqual/IsPathRooted asserts fail); post-fix it returns the rooted trusted path.

Regression sweep

  • dotnet build main + tests: 0 warnings / 0 errors.
  • Leak-term + debug-code scan on all 4 touched files: clean.
  • Author headers intact; version bumped 1.52.97 → 1.52.98; CHANGELOG entry in this PR.

…-planting LPE

winget operations (Bulk Installer install/list/search, App Updates upgrade,
Uninstaller list) route through PowerShellRunner.RunProcessAsync("winget", ...),
which hardens the target via SystemPaths.ResolveSystemTool. But winget is not a
System32 tool -- it is an MSIX app under
%ProgramFiles%\WindowsApps\Microsoft.DesktopAppInstaller_*__8wekyb3d8bbwe, whose
only PATH entry is a per-user execution alias in the user-writable
%LOCALAPPDATA%\Microsoft\WindowsApps. ResolveSystemTool probes only System32, so
both probes missed and it returned the unrooted bare string "winget". Launched
with UseShellExecute=false, CreateProcess searches the calling process's own load
directory first, so a winget.exe planted next to SysManager's portable .exe (often
run from Downloads, sometimes elevated) would run with the app's rights. The
previously-relied-on WorkingDirectory=System32 pin does not cover this (CWD is
search item #2; the app-load dir is #1).

Adds SystemPaths.ResolveWinget: resolves winget to the highest-versioned App
Installer package under the admin-only-writable WindowsApps folder (matched by the
fixed publisher hash and by actually containing winget.exe), and fails closed --
returns a rooted System32\winget.exe path (normally non-existent, surfacing the
Win32Exception callers already handle) rather than the plantable bare name. The
per-user %LOCALAPPDATA% alias is deliberately never used for the elevated launch.
ResolveSystemTool now delegates winget/winget.exe to it.

Regression tests: winget/winget.exe never resolve to the bare name and are always
rooted; ResolveWinget never points under user-writable LocalAppData; version
segment parses numerically (1.29 > 1.9). Runtime-verified via a temp harness
(resolves to the real trusted path, launches winget successfully).

Closes the last winget binary-planting vector after the v1.52.91 (powershell) and
v1.52.92 (Bulk Installer CWD) hardening.
@laurentiu021 laurentiu021 merged commit 91f0303 into main Jul 11, 2026
4 checks passed
@laurentiu021 laurentiu021 deleted the fix/winget-path-lpe branch July 11, 2026 00:04
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.

1 participant