FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

SetRunspacePrompt() injects VM/container name into PowerShell script without escaping — arbitrary code executes on every prompt render · Issue #28255 · PowerShell/PowerShell · GitHub

Repository navigation

SetRunspacePrompt() injects VM/container name into PowerShell script without escaping — arbitrary code executes on every prompt render #28255

Open

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 timestamp

Output 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; VMName property line 158)
  • src/System.Management.Automation/resources/RemotingErrorIdStrings.resx (EnterVMSessionPrompt key)
  • src/System.Management.Automation/utils/StringUtil.cs (Format overloads)

Activity

  1. lupin-sudo commented on Oct 24, 2026

    Author

    Reported by Nader Tahan (lupin-sudo)

  2. kilasuit commented on Nov 8, 2026

    Collaborator

    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.

    • The only possible use case I could think of maybe actually returning the in VM ComputerName which can be different to the referenced Hyper-V VM name in the prompt.

    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.

    To confirm this I created a Server 2019 VM called svr19
    Using 7.6.9

    • I entered a PSSession to confirm the prompt shows svr19 as expected
    • I set the VM Name in Hyper-V Manager to svr19-$($env:computername)
    • I entered a new PSSession to this VM by $vm = (get-vm | where Name -Match computername).name[0] followed by Enter-PSSession -VMName $vm -Credential $vmCreds - which as expected returned [svr19-WIN-G78O17CQJPE]: PS C:\Users\Administrator\Documents>
    • I updated the VMName to '$(Write-Output "INJECTED @ $(Get-Date -Format o)")'
    • I entered a new PSSession to this VM by $vm = (get-vm | where Name -Match Output).name[0] followed by Enter-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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    In-PRIndicates that a PR is out for the issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL