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

Redact targetName passed to remote prompt if it potentially includes PowerShellCode by kilasuit · Pull Request #28378 · PowerShell/PowerShell · GitHub

Repository navigation

Redact targetName passed to remote prompt if it potentially includes PowerShellCode - #28378

Draft
Ryan Yates (kilasuit) wants to merge 9 commits into
PowerShell:masterfrom
kilasuit:28255-remote-runspace-prompt
Draft

Ryan Yates (kilasuit) wants to merge 9 commits into
PowerShell:masterfrom
kilasuit:28255-remote-runspace-prompt

Conversation

Ryan Yates (kilasuit) commented Nov 8, 2026 •
edited
Loading

Copy link
Copy Markdown
Collaborator

PR Summary

This PR adds a redaction code path to negate potential remote prompt injections based on issue

It does so by a new internal static string method RedactIfPowerShellCode which checks if the targetName being passed may start with or contain a token that would be needed to passthrough unwanted code to the remote session's prompt & instead passes a string of REDACTED for the targetName to the prompt, which stops the passing through of any executable code from to the remote sessions prompt.

Confirmed locally that the code proposed fixes the following statements

  • targetName starts with or contains a number of potentially dangerous tokens

with the below PowerShell

$Credential = Get-Credential
# Before this fix
$vm1 = get-vm | where Name -Match output
$vm1.Name # Should be '$(Write-Output "INJECTED @ $(Get-Date -Format o)")'
enter-PSSession -VMName $vm1.Name -Credential $credential
# Shows the injected command output, not the VM name
# ['INJECTED @ 2026-11-08T12:12:32.6331633+00:00']: PS C:\Users\Administrator\Documents>

$vm1 | rename-vm -NewName svr19
$vm1.Name
enter-PSSession -VMName $vm1.Name -Credential $credential
# Shows the expected command output
# [svr19]: PS C:\Users\Administrator\Documents>

# Rename the VM to a name that includes a command injection attempt
$vm1 | Rename-VM -NewName @'
'$(Write-Output "INJECTED @ $(Get-Date -Format o)")'
'@
# The above is the only way to include the SingleQuotes at each end of the name - otherwise this can be set manually in Hyper-V Manager
# Start a new PowerShell process that has been built with the fixes from this PR
start-process C:\ps\i\28255-out-redacted\pwsh.exe -ArgumentList '-noprofile' -Verb runas
$vm1 = get-vm | where Name -Match output
$vm1.Name # Should be '$(Write-Output "INJECTED @ $(Get-Date -Format o)")'
enter-PSSession -VMName $vm1.Name -Credential $credential
# Shows a redacted command output, not the VM name
# [REDACTED]: PS C:\Users\Administrator\Documents>

# Check if contains use of $ in the name
$vm1 | Rename-VM -NewName 'svr19-$($env:computername)'
enter-PSSession -VMName $vm1.Name -Credential $credential
# Shows a redacted command output, not the VM name
# [REDACTED]: PS C:\Users\Administrator\Documents>

$vm1S = New-PSSession -VMName $vm1.Name -Credential $Credential
Enter-PSSession -Session $vm1s
# Shows a redacted command output, not the VM name
# [REDACTED]: PS C:\Users\Administrator\Documents>

PR Context

Fixes #28255

Notes

Tests

I did go looking to see if there was a simple and elegant programmatic way to capture or return the prompt definition from a remote session. However the prompt used seems to be replace the one that is included in the remote sessions initial runspace's InitialSessionState. I also could not return this new prompt via Invoke-Command, likely as it's only set when connected to that remote runspace.

Because of this I've marked this PR as can only be tested interactively.

Breaking Changes & Documentation

Whilst this is a breaking change & will require updates to existing documentation, I feel that this update is required to guard against what most admins likely would not have expected to be feasible & would be unexpected behaviour by not implementing this change & allowing this to continue via this current code path.

This change has no impact to admins setting their own prompt once inside a remote session.

Screenshots

Below shows this having been run in a local build

& the prompt being different between InitialSessionState & the available prompt function

PR Checklist

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Ryan Yates (kilasuit) added WG-Engine core PowerShell engine, interpreter, and runtime CL-Engine Indicates that a PR should be marked as an engine change in the Change Log WG-NeedsReview Needs a review by the labeled Working Group labels Nov 8, 2026
Ryan Yates (kilasuit) marked this pull request as ready for review November 8, 2026 15:38
Copilot AI balanced review requested due to automatic review settings November 8, 2026 15:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

string promptFn = StringUtil.Format(RemotingErrorIdStrings.EnterVMSessionPrompt,
@"function global:prompt { """,
targetName,
targetNameRedacted,

Jordan Borean (jborean93) Nov 9, 2026 •
edited
Loading

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Instead of redacting why don't we use [System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent(...) to escape the target name so it can be safely used in single quotes. We just need to ensure the template we use has single quotes for the targetName and not double quotes but this means we don't need to scan for a dangerous token and keep it always in sync with new tokens or expressions added in the future. It also means the actual value will be shown literally in the prompt and not REDACTED.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I did in the issue this is linked to try using that method, as it had been suggested as an suggested fix.
However in local testing of this with a Client Hyper-V Server2019 VM on W11 26H2 using the code provided it still can execute code in the prompt in the remote VM Session when that's likely not wanted, or if it is wanted, then it's certainly should not be exposed via this mechanism.

I had also tried making use of the AST as a way to determine if what is passed as the targetName had potential to be executable at all, but then rolled that back to this new internal method & only because I didn't know of anything already existing or spend the time to scan & try find it if it did exist.

The choice of using REDACTED here vs not passing anything at all, is that whilst it may seem that It also means the actual value will be shown literally in the prompt I was not convinced this to be the case from the reported string, as well as my own sample test string.

That and well picking REDACTED as it's a well enough known common string that for any documentation updates of this PR is taken would make searches easy & standout for those seeing it.

The below from another local build I'd ran

string escapedTargetName = CodeGeneration.EscapeSingleQuotedStringContent(targetName);
                WriteVerbose($"Setting prompt for pushed runspace to: {escapedTargetName}");
                //string targetNameRedacted = RedactIfPowerShellCode(targetName);
                string promptFn = StringUtil.Format(RemotingErrorIdStrings.EnterVMSessionPrompt,
                    @"function global:prompt { """,
                    escapedTargetName,
                    @"PS $($executionContext.SessionState.Path.CurrentLocation)> "" }");

where you can see the suggestion did not work

I'm sure that there's likely a more sensible approach than this that I missed & would be happy to take that one instead.
This was a relatively quick and dirty approach that I found to resolve the raised issue.

Jordan Borean (jborean93) Nov 9, 2026 •
edited
Loading

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

That's because you kept the quotes as double quotes and didn't turn them into single quotes. The string template for EnterVMSessionPrompt is

{0}[{1}]: {2}

The only different one is the .ru.resx entry but that just has spaces so it's not a problem

{0} [ {1} ]: {2}

The code right now uses double quote @"function global:prompt { """ where the last " ends the string and "" before that turns into 1 " due to escaping. So in pwsh land this looks like

$targetName = 'dangerous$(whoami)'
$escapedTargetName = [System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent(
    $targetName)
# still the same as $ is safe within a single quoted string
'{0}[{1}]: {2}' -f @(
    'function global:prompt {"',
    $escapedTargetName,
    'PS $($executionContext.SessionState.Path.CurrentLocation)> " }'
)

This produces the following string which is still dangerous due to the double quotes. You can see why keeping the double quotes here isn't ideal.

function global:prompt {"[dangerous$(whoami)]: PS $($executionContext.SessionState.Path.CurrentLocation)> " }

Instead to make it safe you need to use single quotes:

string escapedTargetName = System.Management.Automation.Language.CodeGeneration.EscapeSingleQuotedStringContent(targetName);
string promptFn = StringUtil.Format(RemotingErrorIdStrings.EnterVMSessionPrompt,
    @"function global:prompt { '",
    escapedTargetName,
    @"PS ' + $executionContext.SessionState.Path.CurrentLocation + '> ' }");

This keeps the dyanamic value as escaped while also supporting the expression for getting the current location in the prompt. In this example I just temporarily changed the code to use this for SSH targets because I don't have a Hyper-V VM setup to test on but you can see that the target name had an expression and was escaped when using single quotes.

If it was a value with single quotes EscapeSingleQuotedStringContent also handles that for us automatically

If you wanted to make it simpler you can get rid of the template entirely but that's not necessary, just an optimisation as the resource template isn't doing much here. IMO it's simpler and easier to understand but that's just my personal opinion.

string promptFn = StringUtil.Format(
    "function global:prompt {{ '[{0}]: ' + $executionContext.SessionState.Path.CurrentLocation + '> ' }}",
    escapedTargetName);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

That's because you kept the quotes as double quotes and didn't turn them into single quotes.

No it's because I intentionally hadn't looked to change the rest of the makeup of how promptFn is built, even if simplifying how it does so makes sense. It can be done inside or outside of this PR but for now I think it's something that we can return to.
Mainly because I don't really think that if it is properly escaped we should pass the content through to the remote session in this case.
I feel that we only ever display a targetName that actually makes sense & should prevent passing any nonsense string that could be passed through to the end session at all. We should output a Verbose message of sorts along the lines of ya tried to do this, but we stopped it and didn't pass to the end session your unsanitized code which I am thinking should be done like this prior to creating the promptFn string

WriteVerbose($" Attempted to set {targetName} as the prompt for the pushed runspace.");
WriteVerbose($" Actually set: {targetNameRedacted}");

The only thing I'm not too sure on is whether this accounts for existing behaviour for ssh connections & I'm guessing without testing may use '@' - if so then this'll need a minor rework

bool startsWithOrContainsDangerousToken =
                input.StartsWith('$') ||
                input.StartsWith("@{") ||
                input.StartsWith('(') ||
                input.StartsWith('"') ||
                input.StartsWith('@') ||
                input.StartsWith('\'') ||
                input.Contains('$');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Keeping it as a double quoted string just opens it up for future abuse. It also makes the UX for the end user poor when they have a VM name with something like @ as now their prompt won't actually show that anymore.

If we are touching this code to fix the security fix we may as well use the mechanisms in PowerShell to escape this properly than add another manual escape method that would need to be considered if the parser ever changes in the future.

Ryan Yates (kilasuit) Nov 10, 2026 •
edited
Loading

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Keeping it as a double quoted string just opens it up for future abuse.

Yeah from review & chat here, I Agree, and is something that I'll push a change to tackle.

It also makes the UX for the end user poor when they have a VM name with something like @ as now their prompt won't actually show that anymore.

This was unintentional & will also push a change for this.

I still think we shouldn't just pass through to the Remote Runspace's prompt anything that most people would think isn't a realistically suitable targetName, escaped or otherwise. We should code around this providing a way that on discovery, mitigates & crucially provides a way to log this mitigation.

A possible future enhancement would be to make available a mechanism for passing a predefined prompt over to the remote runspace, but that is out of scope of what is needed here & much more work.

I've marked this as draftPR whilst I make changes based on your comments & build up the additional tests I think are needed for this & expect further pushes to happen in next 24hours.

Ryan Yates (kilasuit) marked this pull request as draft November 10, 2026 19:00
Ryan Yates (kilasuit) force-pushed the 28255-remote-runspace-prompt branch from afa9957 to b37a9f2 Compare November 11, 2026 20:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-Engine Indicates that a PR should be marked as an engine change in the Change Log WG-Engine core PowerShell engine, interpreter, and runtime WG-NeedsReview Needs a review by the labeled Working Group

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants


Back | FazBrowse Home | New Git URL