From ce459cdec4c93e77477ab7cf879961a774428685 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 20 May 2026 21:13:44 +0200 Subject: [PATCH] hardened single-quote escape --- .../Monitoring/Runtime/SelfUpdater.cs | 7 ++++++- src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt | 2 +- src/Agent/AutopilotMonitor.Agent/buildcounter.txt | 2 +- src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs | 11 ++++++++--- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/SelfUpdater.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/SelfUpdater.cs index feb14aae..6c78ea57 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/SelfUpdater.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/SelfUpdater.cs @@ -601,10 +601,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime var pid = Process.GetCurrentProcess().Id; var agentExePath = Path.Combine(agentDir, "AutopilotMonitor.Agent.exe"); + // PowerShell single-quoted strings only need ' escaped as ''. Defends against + // paths containing apostrophes (redirected ProgramData, OEM-customized roots); + // without escaping such a path breaks out of the string literal in EncodedCommand. + var safeAgentExePath = agentExePath.Replace("'", "''"); + // PowerShell Wait-Process: waits for our process to actually exit (no polling), // then immediately starts the new agent. 30s timeout as safety net. var psScript = $"Wait-Process -Id {pid} -Timeout 30 -ErrorAction SilentlyContinue; " + - $"& '{agentExePath}'"; + $"& '{safeAgentExePath}'"; var encoded = Convert.ToBase64String(Encoding.Unicode.GetBytes(psScript)); var psi = new ProcessStartInfo diff --git a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt index 58621e61..60d194b0 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt @@ -1 +1 @@ -820 +821 diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 6ee29474..7a1ed495 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -1384 +1385 diff --git a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs index b6259e7f..37ae3ad2 100644 --- a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs +++ b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs @@ -168,6 +168,11 @@ namespace AutopilotMonitor.SummaryDialog var pid = Process.GetCurrentProcess().Id; + // PowerShell single-quoted strings only need ' escaped as ''. Defends against + // staging paths containing apostrophes; without escaping such a path breaks + // out of the string literal in EncodedCommand. + var safeExeDir = exeDir.Replace("'", "''"); + // PowerShell Wait-Process: waits for our actual process exit (no polling), // then short delay for file handle cleanup, then Remove-Item. // Retry loop handles transient file locks (AV scanners, OS handle cleanup). @@ -176,9 +181,9 @@ namespace AutopilotMonitor.SummaryDialog var psScript = $"Wait-Process -Id {pid} -Timeout 30 -ErrorAction SilentlyContinue; " + $"for ($i = 0; $i -lt 3; $i++) {{ " + $"Start-Sleep -Seconds 2; " + - $"Remove-Item -LiteralPath '{exeDir}' -Recurse -Force -ErrorAction SilentlyContinue; " + - $"Remove-Item -LiteralPath '{exeDir}' -Force -ErrorAction SilentlyContinue; " + - $"if (-not (Test-Path -LiteralPath '{exeDir}')) {{ break }} " + + $"Remove-Item -LiteralPath '{safeExeDir}' -Recurse -Force -ErrorAction SilentlyContinue; " + + $"Remove-Item -LiteralPath '{safeExeDir}' -Force -ErrorAction SilentlyContinue; " + + $"if (-not (Test-Path -LiteralPath '{safeExeDir}')) {{ break }} " + $"}}"; var encoded = Convert.ToBase64String(Encoding.Unicode.GetBytes(psScript));