Repository navigation
Description
Summary
PushRunspaceCommand.SetRunspacePrompt() (line 1138, src/System.Management.Automation/engine/remoting/commands/PushRunspaceCommand.cs, current HEAD bf456e85) builds a function global:prompt { … } body by string-interpolating the VM/container name directly into a double-quoted PowerShell string, with no escaping.
Because the result is executed with ps.AddScript(promptFn).Invoke(), any $(…) subexpression embedded in the name is evaluated — once when the function is defined, and again on every subsequent prompt draw, because that is how the host prompt works.
Affected code
// PushRunspaceCommand.cs ~line 1178
string promptFn = StringUtil.Format(RemotingErrorIdStrings.EnterVMSessionPrompt,
@"function global:prompt { """,
targetName, // ← no escaping
@"PS $($executionContext.SessionState.Path.CurrentLocation)> "" }");
ps.AddScript(promptFn).Invoke();targetName is set from this.VMName, this.ContainerId, this.Session.ComputerName, or remoteRunspace.ConnectionInfo.ComputerName depending on the parameter set. None of these values are validated or escaped before use. StringUtil.Format is a plain string.Format(CultureInfo.CurrentCulture, …) wrapper with no sanitization (confirmed in StringUtil.cs).
The template in RemotingErrorIdStrings.resx is {0}[{1}]: {2} — slot {1} receives targetName unmodified.
Reproduction (no Hyper-V required)
The following reproduces the exact string-building and execution logic used by the real code path:
$template = "{0}[{1}]: {2}"
$maliciousName = '$(Write-Output "INJECTED @ $(Get-Date -Format o)")'
$arg0 = 'function global:prompt { "'
$arg2 = 'PS $($executionContext.SessionState.Path.CurrentLocation)> " }'
$promptFn = $template -f $arg0, $maliciousName, $arg2
Invoke-Expression $promptFn
prompt # fires injection #1
prompt # fires injection #2, different timestampOutput shows two different timestamps — confirming re-execution on every prompt call, not just at definition time.
Real-world trigger
Any user who can name or rename a Hyper-V VM or Windows Container to a string containing $(…) causes injected code to execute inside the session of any user who runs Enter-PSSession -VMName (or -VMId, -ContainerId) against it. The injected code re-runs after every command typed in that session.
Suggested fix
Option A — escape targetName before interpolation using the API already used correctly elsewhere in the codebase (CodeGeneration.EscapeSingleQuotedStringContent):
string safeName = CodeGeneration.EscapeSingleQuotedStringContent(targetName);
string promptFn = StringUtil.Format(RemotingErrorIdStrings.EnterVMSessionPrompt,
@"function global:prompt { '",
safeName,
@"' + "" PS $($executionContext.SessionState.Path.CurrentLocation)> "" }");Option B — stop building the prompt as script text. Pass targetName into the runspace as a variable and reference it from a script block, so it is never evaluated as PowerShell syntax.
Affected files
src/System.Management.Automation/engine/remoting/commands/PushRunspaceCommand.cs(SetRunspacePrompt()line 1138;VMNameproperty line 158)src/System.Management.Automation/resources/RemotingErrorIdStrings.resx(EnterVMSessionPromptkey)src/System.Management.Automation/utils/StringUtil.cs(Format overloads)
Activity
lupin-sudo commentedon Oct 24, 2026
Reported by Nader Tahan (lupin-sudo)
kilasuit commentedon Nov 8, 2026
I'm trying to work out if there is a use case where this actually might be the wanted behaviour at all, though I somehow doubt it.
To confirm this I created a Server 2019 VM called svr19
Using 7.6.9
svr19-$($env:computername)$vm = (get-vm | where Name -Match computername).name[0]followed byEnter-PSSession -VMName $vm -Credential $vmCreds- which as expected returned[svr19-WIN-G78O17CQJPE]: PS C:\Users\Administrator\Documents>'$(Write-Output "INJECTED @ $(Get-Date -Format o)")'$vm = (get-vm | where Name -Match Output).name[0]followed byEnter-PSSession -VMName $vm -Credential $vmCreds- which returned[''INJECTED @ 2026-11-08T08:18:43.3939799+00:00'']: PS C:\Users\Administrator\Documents>I then built a local build of PowerShell that implemented your suggested Option A & tried the steps above again. This suggestion however didn't seem to mitigate this.
Your Option B lead me to building out this branch & then raising this PR to mitigate this.
I went down a bit of a rabbit hole by trying to make use of the AST to guard around this as can be seen in commit f229dc0 & decided to instead simplify this in commit afa9957