Skip to content

fix: Bulk Installer winget binary-planting LPE + search argument injection#1420

Merged
laurentiu021 merged 3 commits into
mainfrom
fix/bulkinstaller-winget-lpe-injection
Jul 10, 2026
Merged

fix: Bulk Installer winget binary-planting LPE + search argument injection#1420
laurentiu021 merged 3 commits into
mainfrom
fix/bulkinstaller-winget-lpe-injection

Conversation

@laurentiu021

Copy link
Copy Markdown
Owner

Summary

Two fresh ultra-audit findings (security-deep) in BulkInstallerViewModel, both confirmed by 3 adversarial verifier lenses (isReal=true, high confidence):

  • P1 (LPE): winget launched by bare name from a hand-built process with no pinned working directory.
  • P3 (injection): the raw search box text interpolated into the winget argument string.

The defects

MarkInstalledAppsAsync (runs automatically on tab open) and the package search each built their own ProcessStartInfo { FileName = "winget", UseShellExecute = false } with no WorkingDirectory. With UseShellExecute=false, Win32 CreateProcess searches the calling process's own directory before resolving the real winget App Execution Alias — so an attacker-planted winget.exe beside SysManager's single portable .exe (often run from a user-writable folder like Downloads, sometimes elevated) would execute with the app's privileges. Separately, the search argument was $"search \"{query}\" …" built from the unvalidated search box — the one process-launch trust boundary in the app without input validation — so a query containing a double-quote could break out and inject extra winget arguments.

The fix

Both winget calls now route through BulkInstallerService (the IPowerShellRunner seam):

  • The runner launches winget with WorkingDirectory pinned to System32, removing the CWD from the CreateProcess search order — matching every other winget call in the app and closing the binary-planting vector.
  • The search query is sanitized (double-quotes + control characters stripped, then trimmed) before interpolation; an empty-after-sanitize query never launches a process.
  • Also brings the two calls into Gate-ARCH conformance — external process launches route through the single runner seam instead of a bespoke ProcessStartInfo. The obsolete SearchWingetPackages VM method is removed.

Regression tests

  • SanitizeQuery_StripsQuotesAndControlChars / SanitizeQuery_EmptyOrUnusable_ReturnsEmpty — the injection guard.
  • SearchAsync_RoutesThroughRunner_WithSanitizedQuery — dangerous quotes are gone from the argument string that reaches winget; the call goes through the seam.
  • SearchAsync_EmptyAfterSanitize_NeverRunsProcess — a quotes-only query launches nothing.
  • ListInstalledAsync_RoutesThroughRunner — the list path goes through the seam (WorkingDirectory pinned).

Checks

  • All 3 projects build 0 warnings / 0 errors
  • Leak scan on diff: clean

…ction

MarkInstalledAppsAsync (auto-runs on tab open) and the package search each built
their own ProcessStartInfo { FileName = "winget", UseShellExecute = false } with NO
WorkingDirectory. With UseShellExecute=false, Win32 CreateProcess searches the
calling process's own directory before the real winget App Execution Alias, so an
attacker-planted winget.exe beside SysManager's portable .exe (often run from a
user-writable folder, sometimes elevated) would run with the app's privileges.
Separately, the search argument was $"search \"{query}\" …" built from the
unvalidated search box — a double-quote could break out and inject winget args.

Both calls now route through BulkInstallerService (IPowerShellRunner seam), which
launches winget with WorkingDirectory pinned to System32 (matching every other
winget call and removing the CWD from the search order), and the search query is
sanitized (quotes + control chars stripped) before interpolation. Also brings the
two calls into Gate-ARCH conformance (external process launches route through the
single runner seam, not a bespoke ProcessStartInfo). The obsolete
SearchWingetPackages VM method is removed.

Source: fresh multi-agent ultra-audit (security-deep), P1 (LPE) + P3 (injection),
confirmed by 3 adversarial verifier lenses (isReal=true, high confidence).

Regression tests: SanitizeQuery_StripsQuotesAndControlChars,
SanitizeQuery_EmptyOrUnusable_ReturnsEmpty, SearchAsync_RoutesThroughRunner_
WithSanitizedQuery, SearchAsync_EmptyAfterSanitize_NeverRunsProcess,
ListInstalledAsync_RoutesThroughRunner.
The two failing assertions expected 'foo  calc' but SanitizeQuery only strips
double-quotes and control chars — not '&' — so 'foo" & calc "' correctly yields
'foo & calc' (the & is harmless inside a quoted, shell-less winget argument; the
security property is that the embedded quotes can't break out of the token).
Corrected the [InlineData] expectation and the SearchAsync arg assertion (now
checks the query is wrapped in exactly one quote pair). Hand-verified all five
Theory rows + the empty cases against the real logic before pushing.
@laurentiu021 laurentiu021 merged commit 7c08564 into main Jul 10, 2026
4 checks passed
@laurentiu021 laurentiu021 deleted the fix/bulkinstaller-winget-lpe-injection branch July 10, 2026 21:30
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