diff --git a/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs b/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs
index 13dfe839b9..32c68e961c 100644
--- a/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs
+++ b/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs
@@ -489,37 +489,6 @@ Function PSGetSerializedShowCommandInfo
@"Remove-Item -Path 'function:\PSGetSerializedShowCommandInfo' -Force");
}
- ///
- /// Gets the command to be run to in order to import a module and refresh the command data.
- ///
- /// Module we want to import.
- /// Boolean flag determining whether Show-Command is queried in the local or remote runspace scenario.
- /// Boolean flag to indicate that it is the second attempt to query Show-Command data.
- /// The command to be run to in order to import a module and refresh the command data.
- internal static string GetImportModuleCommand(string module, bool isRemoteRunspace = false, bool isFirstChance = true)
- {
- string scriptBase = "Import-Module " + ShowCommandHelper.SingleQuote(module);
-
- if (isRemoteRunspace)
- {
- if (isFirstChance)
- {
- scriptBase += ";@(Get-Command " + ShowCommandHelper.CommandTypeSegment + @" -ShowCommandInfo )";
- }
- else
- {
- scriptBase += GetSerializedCommandScript();
- }
- }
- else
- {
- scriptBase += ";@(Get-Command " + ShowCommandHelper.CommandTypeSegment + ")";
- }
-
- scriptBase += ShowCommandHelper.GetGetModuleSuffix();
- return scriptBase;
- }
-
///
/// Gets the command to be run in order to show help for a command.
///
diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs
index e31cf2f81a..1ad23a40cc 100644
--- a/src/System.Management.Automation/engine/Utils.cs
+++ b/src/System.Management.Automation/engine/Utils.cs
@@ -1256,7 +1256,8 @@ namespace System.Management.Automation
#if UNIX
return false;
#else
- return path.StartsWith(@"\\.\") || path.StartsWith(@"\\?\");
+ // device paths can be network paths, we would need windows to parse it.
+ return path.StartsWith(@"\\.\") || path.StartsWith(@"\\?\") || path.StartsWith(@"\\;");
#endif
}
@@ -1523,6 +1524,23 @@ namespace System.Management.Automation
_ => $"0 Bytes",
};
}
+
+ ///
+ /// Returns true if the current session is restricted (JEA or similar sessions)
+ ///
+ /// ExecutionContext.
+ /// True if the session is restricted.
+ internal static bool IsSessionRestricted(ExecutionContext context)
+ {
+ CmdletInfo cmdletInfo = context.SessionState.InvokeCommand.GetCmdlet("Microsoft.PowerShell.Core\\Import-Module");
+ // if import-module is visible, then the session is not restricted,
+ // because the user can load arbitrary code.
+ if (cmdletInfo != null && cmdletInfo.Visibility == SessionStateEntryVisibility.Public)
+ {
+ return false;
+ }
+ return true;
+ }
}
}
diff --git a/src/System.Management.Automation/help/HelpCommands.cs b/src/System.Management.Automation/help/HelpCommands.cs
index af79758dcb..1b451637a5 100644
--- a/src/System.Management.Automation/help/HelpCommands.cs
+++ b/src/System.Management.Automation/help/HelpCommands.cs
@@ -255,6 +255,17 @@ namespace Microsoft.PowerShell.Commands
///
protected override void ProcessRecord()
{
+#if !UNIX
+ string fileSystemPath = SessionState.Path.GetUnresolvedProviderPathFromPSPath(this.Name);
+ string normalizedName = FileSystemProvider.NormalizePath(fileSystemPath);
+ // In a restricted session, do not allow help on network paths or device paths, because device paths can be used to bypass the restrictions.
+ if (Utils.IsSessionRestricted(this.Context) && (FileSystemProvider.PathIsNetworkPath(normalizedName) || Utils.PathIsDevicePath(normalizedName))) {
+ Exception e = new ArgumentException(HelpErrors.NoNetworkCommands, "Name");
+ ErrorRecord errorRecord = new ErrorRecord(e, "CommandNameNotAllowed", ErrorCategory.InvalidArgument, null);
+ this.ThrowTerminatingError(errorRecord);
+ }
+#endif
+
HelpSystem helpSystem = this.Context.HelpSystem;
try
{
@@ -504,7 +515,7 @@ namespace Microsoft.PowerShell.Commands
}
///
- /// Validates input parameters.
+ /// Validates input parameters.
///
/// Category specified by the user.
///
diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs
index 7236a6f389..9337b46680 100644
--- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs
+++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs
@@ -105,7 +105,7 @@ namespace Microsoft.PowerShell.Commands
///
/// The path with all / normalized to \
///
- private static string NormalizePath(string path)
+ internal static string NormalizePath(string path)
{
return GetCorrectCasedPath(path.Replace(StringLiterals.AlternatePathSeparator, StringLiterals.DefaultPathSeparator));
}
diff --git a/src/System.Management.Automation/resources/HelpErrors.resx b/src/System.Management.Automation/resources/HelpErrors.resx
index 851e457cf2..27634de299 100644
--- a/src/System.Management.Automation/resources/HelpErrors.resx
+++ b/src/System.Management.Automation/resources/HelpErrors.resx
@@ -187,4 +187,7 @@ To update these Help topics, start PowerShell by using the "Run as Administrator
ForwardHelpTargetName cannot refer to the function itself.
+
+ Cannot get help from a network location when in a restricted session.
+
diff --git a/test/powershell/engine/Help/HelpSystem.OnlineHelp.Tests.ps1 b/test/powershell/engine/Help/HelpSystem.OnlineHelp.Tests.ps1
index b56effc862..3e06c51f6c 100644
--- a/test/powershell/engine/Help/HelpSystem.OnlineHelp.Tests.ps1
+++ b/test/powershell/engine/Help/HelpSystem.OnlineHelp.Tests.ps1
@@ -1,6 +1,8 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
+Import-Module HelpersCommon
+
Describe 'Online help tests for PowerShell Cmdlets' -Tags "Feature" {
# The csv files (V2Cmdlets.csv and V3Cmdlets.csv) contain a list of cmdlets and expected HelpURIs.
@@ -61,3 +63,18 @@ Describe 'Get-Help -Online is not supported on Nano Server and IoT' -Tags "CI" {
{ Get-Help Get-Help -Online } | Should -Throw -ErrorId "InvalidOperation,Microsoft.PowerShell.Commands.GetHelpCommand"
}
}
+
+Describe 'Get-Help should throw on network paths' -Tags "CI" {
+ BeforeAll {
+ $script:skipTest = -not $IsWindows
+ }
+
+ It "Get-Help should throw not on " -Skip:$skipTest -TestCases (Get-HelpNetworkTestCases -PositiveCases) {
+ param(
+ $Command,
+ $ExpectedError
+ )
+
+ { Get-Help -Name $Command } | Should -Not -Throw
+ }
+}
diff --git a/test/powershell/engine/Remoting/RemoteSession.Basic.Tests.ps1 b/test/powershell/engine/Remoting/RemoteSession.Basic.Tests.ps1
index 8efe61de35..43f92cef29 100644
--- a/test/powershell/engine/Remoting/RemoteSession.Basic.Tests.ps1
+++ b/test/powershell/engine/Remoting/RemoteSession.Basic.Tests.ps1
@@ -106,7 +106,6 @@ Describe "JEA session Transcript script test" -Tag @("Feature", 'RequireAdminOnW
Unregister-PSSessionConfiguration -Name JEA -Force -ErrorAction SilentlyContinue
}
}
-
}
Describe "JEA session Get-Help test" -Tag @("CI", 'RequireAdminOnWindows') {
@@ -155,6 +154,34 @@ Describe "JEA session Get-Help test" -Tag @("CI", 'RequireAdminOnWindows') {
Remove-Item $RoleCapDirectory -Recurse -Force -ErrorAction SilentlyContinue
}
}
+
+ It "Get-Help should throw on " -TestCases (Get-HelpNetworkTestCases) {
+ param(
+ $Command,
+ $ExpectedError
+ )
+
+ [string] $RoleCapDirectory = (New-Item -Path "$TestDrive\RoleCapability" -ItemType Directory -Force).FullName
+ [string] $PSSessionConfigFile = "$RoleCapDirectory\TestConfig.pssc"
+ $configurationName = 'RestrictedWithNoGetHelpProxy'
+ try
+ {
+ New-PSSessionConfigurationFile -Path $PSSessionConfigFile `
+ -SessionType Empty `
+ -LanguageMode NoLanguage `
+ -ModulesToImport 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Core' `
+ -VisibleCmdlets 'Get-command', 'measure-object', 'select-object', 'enter-pssession', 'get-formatdata', 'out-default', 'out-file', 'exit-pssession', 'get-help'
+ Register-PSSessionConfiguration -Name $configurationName -Path $PSSessionConfigFile -Force -ErrorAction SilentlyContinue
+ $scriptBlock = [scriptblock]::Create("Get-Help -Name $Command")
+ {Invoke-Command -ConfigurationName $configurationName -ComputerName localhost -ScriptBlock $scriptBlock -ErrorAction Stop} |
+ Should -Throw -ErrorId $ExpectedError
+ }
+ finally
+ {
+ Unregister-PSSessionConfiguration -Name $configurationName -Force -ErrorAction SilentlyContinue
+ Remove-Item $RoleCapDirectory -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ }
}
Describe "Remoting loopback tests" -Tags @('CI', 'RequireAdminOnWindows') {
@@ -358,6 +385,7 @@ Describe "Remoting loopback tests" -Tags @('CI', 'RequireAdminOnWindows') {
$session = New-RemoteSession -ConfigurationName $endPoint
try {
$result = Invoke-Command -Session $session -ScriptBlock { $Host.Version }
+ Write-Verbose "host version: $result" -Verbose
$result | Should -Be $PSVersionTable.PSVersion
}
finally {
diff --git a/test/tools/Modules/HelpersCommon/HelpersCommon.psd1 b/test/tools/Modules/HelpersCommon/HelpersCommon.psd1
index 897d41c251..dd7cd7a52a 100644
--- a/test/tools/Modules/HelpersCommon/HelpersCommon.psd1
+++ b/test/tools/Modules/HelpersCommon/HelpersCommon.psd1
@@ -48,6 +48,7 @@ FunctionsToExport = @(
'Test-PSDefaultParameterValue'
'Push-DefaultParameterValueStack'
'Pop-DefaultParameterValueStack'
+ 'Get-HelpNetworkTestCases'
)
CmdletsToExport= @()
diff --git a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1
index aefd2893a2..50d6a2e668 100644
--- a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1
+++ b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1
@@ -533,3 +533,87 @@ function Pop-DefaultParameterValueStack {
return $false
}
}
+
+function Get-HelpNetworkTestCases
+{
+ param(
+ [switch]
+ $PositiveCases
+ )
+ # .NET doesn't consider these path rooted and we won't go to the network:
+ # \\?
+ # \\.
+ # \??
+
+ # Command discovery does not follow symlinks to network locations for module qualified paths
+ $networkBlockedError = "CommandNameNotAllowed,Microsoft.PowerShell.Commands.GetHelpCommand"
+ $scriptBlockedError = "ScriptsNotAllowed"
+
+ $formats = @(
+ '//{0}/share/{1}'
+ '\\{0}\share\{1}'
+ '//{0}\share/{1}'
+ 'Microsoft.PowerShell.Core\filesystem:://{0}/share/{1}'
+ )
+
+ if (!$PositiveCases) {
+ $formats += 'filesystem:://{0}/share/{1}'
+ }
+
+ $moduleQualifiedCommand = 'test.dll\fakecommand'
+ $lanManFormat = @(
+ '//;LanmanRedirector/{0}/share/{1}'
+ )
+
+ $hosts = @(
+ 'fakehost'
+ 'fakehost.pstest'
+ )
+
+ $commands = @(
+ 'test.ps1'
+ 'test.dll'
+ $moduleQualifiedCommand
+ )
+
+ $variants = @()
+ $cases = @()
+ foreach($command in $commands) {
+ $hostName = $hosts[0]
+ $format = $formats[0]
+ $cases += @{
+ Command = $format -f $hostName, $command
+ ExpectedError = $networkBlockedError
+ }
+ }
+
+ foreach($hostName in $hosts) {
+ # chose the format with backslashes(\) to match the host with blackslashes
+ $format = $formats[1]
+ $command = $commands[0]
+ $cases += @{
+ Command = $format -f $hostName, $command
+ ExpectedError = $networkBlockedError
+ }
+ }
+ foreach($format in $formats) {
+ $hostName = $hosts[0]
+ $command = $commands[0]
+ $cases += @{
+ Command = $format -f $hostName, $command
+ ExpectedError = $networkBlockedError
+ }
+ }
+
+ foreach($format in $lanManFormat) {
+ $hostName = $hosts[0]
+ $command = $moduleQualifiedCommand
+ $cases += @{
+ Command = $format -f $hostName, $command
+ ExpectedError = $scriptBlockedError
+ }
+ }
+
+ return $cases | Sort-Object -Property ExpectedError, Command -Unique
+}
+