diff --git a/CHANGELOG.md b/CHANGELOG.md index a799b54b..b6da86c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,21 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). handler from ViewModel on teardown. Previously 51 subscriptions leaked permanently. `MainWindowViewModel.Dispose()` now disposes all NavItems. +## [0.48.16] - 2026-05-15 + +### Fixed +- **SpeedTestService** — stdout and stderr now read in parallel via + `Task.WhenAll` to prevent classic Windows pipe buffer deadlock when + Ookla CLI writes enough to stderr while stdout is being consumed. +- **DiskHealthService** — added regex validation (`^[\w{}\-\\.:/]+$`) on + WMI objectId before WQL interpolation (defense-in-depth against injection). +- **UninstallerService** — tightened `PackageIdPattern` regex: replaced `\s` + (which allows tabs/newlines) with a literal space character. + +### Changed +- **WindowsFeaturesService** — added SECURITY-CRITICAL documentation comment + on `FeatureNamePattern()` regex explaining it is the sole injection defense. + ## [0.48.14] - 2026-05-15 ### Fixed diff --git a/SysManager/SysManager/Services/DiskHealthService.cs b/SysManager/SysManager/Services/DiskHealthService.cs index 39d2dac7..51d695dc 100644 --- a/SysManager/SysManager/Services/DiskHealthService.cs +++ b/SysManager/SysManager/Services/DiskHealthService.cs @@ -3,6 +3,7 @@ // License: MIT using System.Management; +using Serilog; using SysManager.Models; namespace SysManager.Services; @@ -68,6 +69,15 @@ private static void EnrichWithReliability(ManagementScope scope, string objectId { try { + // Defense-in-depth: validate objectId format before WQL interpolation. + // ObjectId from MSFT_PhysicalDisk is typically like {guid}\\PhysicalDisk0. + // Reject anything that doesn't match expected characters. + if (!System.Text.RegularExpressions.Regex.IsMatch(objectId, @"^[\w{}\-\\.:/]+$")) + { + Log.Warning("DiskHealth: unexpected objectId format, skipping reliability query"); + return; + } + // Escape quotes & backslashes for the WQL literal. var safeId = objectId.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("'", "\\'"); var query = new ObjectQuery( diff --git a/SysManager/SysManager/Services/SpeedTestService.cs b/SysManager/SysManager/Services/SpeedTestService.cs index 75198627..bcd29dcc 100644 --- a/SysManager/SysManager/Services/SpeedTestService.cs +++ b/SysManager/SysManager/Services/SpeedTestService.cs @@ -205,8 +205,16 @@ public async Task RunOoklaAsync( using var proc = new Process(); proc.StartInfo = psi; await Task.Run(() => proc.Start(), linked).ConfigureAwait(false); - var stdout = await proc.StandardOutput.ReadToEndAsync(linked); - var stderr = await proc.StandardError.ReadToEndAsync(linked); + + // Read stdout and stderr in parallel to prevent pipe buffer deadlock. + // If one pipe fills while the other is being read sequentially, the + // child process blocks indefinitely (classic Windows pipe deadlock). + var stdoutTask = proc.StandardOutput.ReadToEndAsync(linked); + var stderrTask = proc.StandardError.ReadToEndAsync(linked); + await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false); + var stdout = stdoutTask.Result; + var stderr = stderrTask.Result; + try { await proc.WaitForExitAsync(linked); diff --git a/SysManager/SysManager/Services/UninstallerService.cs b/SysManager/SysManager/Services/UninstallerService.cs index d69af370..d12e0597 100644 --- a/SysManager/SysManager/Services/UninstallerService.cs +++ b/SysManager/SysManager/Services/UninstallerService.cs @@ -65,7 +65,7 @@ public async Task UninstallAsync(string packageId, CancellationToken ct = d /// Matches valid winget package IDs: alphanumeric, dots, hyphens, /// underscores, forward slashes, plus signs, and spaces. Max 256 chars. /// - [System.Text.RegularExpressions.GeneratedRegex(@"^[\w.\-/+\s]{1,256}$")] + [System.Text.RegularExpressions.GeneratedRegex(@"^[\w.\-/+ ]{1,256}$")] private static partial System.Text.RegularExpressions.Regex PackageIdPattern(); internal static List ParseListTable(List lines) diff --git a/SysManager/SysManager/Services/WindowsFeaturesService.cs b/SysManager/SysManager/Services/WindowsFeaturesService.cs index 7623fbed..f84ed975 100644 --- a/SysManager/SysManager/Services/WindowsFeaturesService.cs +++ b/SysManager/SysManager/Services/WindowsFeaturesService.cs @@ -155,6 +155,9 @@ internal static string HumanizeName(string featureName) /// /// Validates feature names: alphanumeric, hyphens, underscores, dots. Max 128 chars. + /// SECURITY-CRITICAL: This regex is the sole defense against PowerShell injection + /// in Enable/DisableFeatureAsync where featureName is interpolated into a command. + /// Do NOT relax this pattern without reviewing injection implications. /// [GeneratedRegex(@"^[\w.\-]{1,128}$")] private static partial Regex FeatureNamePattern();