diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs
index 0fb85e740f4..0546dc527ee 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs
+++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs
@@ -14,6 +14,7 @@
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
using System.Management.Automation.Runspaces;
+using System.Management.Automation.Security;
using System.Security;
using System.Text;
@@ -878,10 +879,27 @@ internal void Parse(string[] args)
ParseHelper(args);
}
+ internal static bool IsFileOnlyEntryEnabled
+ {
+ get
+ {
+#if UNIX
+ return false;
+#else
+ return SystemPolicy.IsFileOnlyEntryEnabled();
+#endif
+ }
+ }
+
private void ParseHelper(string[] args)
{
if (args.Length == 0)
{
+ if (IsFileOnlyEntryEnabled)
+ {
+ SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired);
+ }
+
return;
}
@@ -927,6 +945,12 @@ private void ParseHelper(string[] args)
_noExit = true;
noexitSeen = true;
ParametersUsed |= ParameterBitmap.NoExit;
+
+ if (IsFileOnlyEntryEnabled)
+ {
+ SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryNoExitDisabled);
+ break;
+ }
}
else if (MatchSwitch(switchKey, "noprofile", "nop"))
{
@@ -948,6 +972,11 @@ private void ParseHelper(string[] args)
_socketServerMode = true;
_showBanner = false;
ParametersUsed |= ParameterBitmap.SocketServerMode;
+ if (IsFileOnlyEntryEnabled)
+ {
+ SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryServerMode);
+ break;
+ }
}
#if !UNIX
else if (MatchSwitch(switchKey, "v2socketservermode", "v2so"))
@@ -955,6 +984,11 @@ private void ParseHelper(string[] args)
_v2SocketServerMode = true;
_showBanner = false;
ParametersUsed |= ParameterBitmap.V2SocketServerMode;
+ if (IsFileOnlyEntryEnabled)
+ {
+ SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryServerMode);
+ break;
+ }
}
#endif
else if (MatchSwitch(switchKey, "servermode", "s"))
@@ -962,18 +996,33 @@ private void ParseHelper(string[] args)
_serverMode = true;
_showBanner = false;
ParametersUsed |= ParameterBitmap.ServerMode;
+ if (IsFileOnlyEntryEnabled)
+ {
+ SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryServerMode);
+ break;
+ }
}
else if (MatchSwitch(switchKey, "namedpipeservermode", "nam"))
{
_namedPipeServerMode = true;
_showBanner = false;
ParametersUsed |= ParameterBitmap.NamedPipeServerMode;
+ if (IsFileOnlyEntryEnabled)
+ {
+ SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryServerMode);
+ break;
+ }
}
else if (MatchSwitch(switchKey, "sshservermode", "sshs"))
{
_sshServerMode = true;
_showBanner = false;
ParametersUsed |= ParameterBitmap.SSHServerMode;
+ if (IsFileOnlyEntryEnabled)
+ {
+ SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryServerMode);
+ break;
+ }
}
else if (MatchSwitch(switchKey, "noprofileloadtime", "noprofileloadtime"))
{
@@ -1263,6 +1312,15 @@ private void ParseHelper(string[] args)
}
}
+ if (_error is null
+ && !_showVersion
+ && !_showHelp
+ && !ParametersUsed.HasFlag(ParameterBitmap.File)
+ && IsFileOnlyEntryEnabled)
+ {
+ SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired);
+ }
+
Dbg.Assert(
((_exitCode == ConsoleHost.ExitCodeBadCommandLineParameter) && _abortStartup)
|| (_exitCode == ConsoleHost.ExitCodeSuccess),
@@ -1360,6 +1418,12 @@ private bool ParseFile(string[] args, ref int i, bool noexitSeen)
// Process interactive input...
if (args[i] == "-")
{
+ if (IsFileOnlyEntryEnabled)
+ {
+ SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired);
+ return false;
+ }
+
// the arg to -file is -, which is secret code for "read the commands from stdin with prompts"
_explicitReadCommandsFromStdin = true;
@@ -1492,6 +1556,12 @@ static object ConvertToBoolIfPossible(string arg)
private bool ParseCommand(string[] args, ref int i, bool noexitSeen, bool isEncoded)
{
+ if (IsFileOnlyEntryEnabled)
+ {
+ SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired);
+ return false;
+ }
+
if (_commandLineCommand != null)
{
// we've already set the command, so squawk
diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
index daf69d50251..c79cf42c57a 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
+++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
@@ -165,8 +165,6 @@ internal static int Start(
// improve startup performance.
}
- uint exitCode = ExitCodeSuccess;
-
Thread.CurrentThread.Name = "ConsoleHost main thread";
try
@@ -192,7 +190,7 @@ internal static int Start(
// or start up the engine and retrieve the information via $psversiontable.GitCommitId
// but this returns the semantic version and avoids executing a script
s_theConsoleHost.UI.WriteLine($"PowerShell {PSVersionInfo.GitCommitId}");
- return 0;
+ return ExitCodeSuccess;
}
// Servermode parameter validation check.
@@ -223,6 +221,14 @@ internal static int Start(
return ExitCodeBadCommandLineParameter;
}
+ if (serverModeCount is 1 && CommandLineParameterParser.IsFileOnlyEntryEnabled)
+ {
+ // User facing error message should already be written by the parser,
+ // so just trace and exit.
+ s_tracer.TraceError("Server mode cannot be specified when FileOnlyEntry policy is in place.");
+ return ExitCodeBadCommandLineParameter;
+ }
+
#if !UNIX
TaskbarJumpList.CreateRunAsAdministratorJumpList();
#endif
@@ -237,9 +243,10 @@ internal static int Start(
configurationName: null,
configurationFile: s_cpp.ConfigurationFile,
combineErrOutStream: false);
- exitCode = 0;
+ return ExitCodeSuccess;
}
- else if (s_cpp.SSHServerMode)
+
+ if (s_cpp.SSHServerMode)
{
ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("SSHServer", s_cpp.ParametersUsedAsDouble);
ProfileOptimization.StartProfile("StartupProfileData-SSHServerMode");
@@ -249,18 +256,20 @@ internal static int Start(
configurationName: null,
configurationFile: s_cpp.ConfigurationFile,
combineErrOutStream: true);
- exitCode = 0;
+ return ExitCodeSuccess;
}
- else if (s_cpp.NamedPipeServerMode)
+
+ if (s_cpp.NamedPipeServerMode)
{
ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("NamedPipe", s_cpp.ParametersUsedAsDouble);
ProfileOptimization.StartProfile("StartupProfileData-NamedPipeServerMode");
RemoteSessionNamedPipeServer.RunServerMode(
configurationName: s_cpp.ConfigurationName);
- exitCode = 0;
+ return ExitCodeSuccess;
}
#if !UNIX
- else if (s_cpp.V2SocketServerMode)
+
+ if (s_cpp.V2SocketServerMode)
{
if (s_cpp.Token == null)
{
@@ -284,50 +293,49 @@ internal static int Start(
token: s_cpp.Token,
tokenCreationTime: s_cpp.UTCTimestamp.Value);
- exitCode = 0;
+ return ExitCodeSuccess;
}
#endif
- else if (s_cpp.SocketServerMode)
+
+ if (s_cpp.SocketServerMode)
{
ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("SocketServerMode", s_cpp.ParametersUsedAsDouble);
ProfileOptimization.StartProfile("StartupProfileData-SocketServerMode");
HyperVSocketMediator.Run(
initialCommand: s_cpp.InitialCommand,
configurationName: s_cpp.ConfigurationName);
- exitCode = 0;
+ return ExitCodeSuccess;
}
- else
+
+ // Run PowerShell in normal console mode.
+ if (hostException != null)
{
- // Run PowerShell in normal console mode.
- if (hostException != null)
- {
- // Unable to create console host.
- throw hostException;
- }
+ // Unable to create console host.
+ throw hostException;
+ }
- if (LoadPSReadline())
- {
- ProfileOptimization.StartProfile("StartupProfileData-Interactive");
+ if (LoadPSReadline())
+ {
+ ProfileOptimization.StartProfile("StartupProfileData-Interactive");
- if (UpdatesNotification.CanNotifyUpdates)
- {
- // Start a task in the background to check for the update release.
- _ = UpdatesNotification.CheckForUpdates();
- }
- }
- else
+ if (UpdatesNotification.CanNotifyUpdates)
{
- ProfileOptimization.StartProfile("StartupProfileData-NonInteractive");
+ // Start a task in the background to check for the update release.
+ _ = UpdatesNotification.CheckForUpdates();
}
+ }
+ else
+ {
+ ProfileOptimization.StartProfile("StartupProfileData-NonInteractive");
+ }
- s_theConsoleHost.BindBreakHandler();
- IsStdOutputRedirected = Console.IsOutputRedirected;
+ s_theConsoleHost.BindBreakHandler();
+ IsStdOutputRedirected = Console.IsOutputRedirected;
- // Send startup telemetry for ConsoleHost startup
- ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("Normal", s_cpp.ParametersUsedAsDouble);
+ // Send startup telemetry for ConsoleHost startup
+ ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("Normal", s_cpp.ParametersUsedAsDouble);
- exitCode = s_theConsoleHost.Run(s_cpp, false);
- }
+ return unchecked((int)s_theConsoleHost.Run(s_cpp, false));
}
finally
{
@@ -349,11 +357,6 @@ internal static int Start(
}
#pragma warning restore IDE0031
}
-
- unchecked
- {
- return (int)exitCode;
- }
}
internal static void ParseCommandLine(string[] args)
diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx
index c84e1b376ed..7a66c4dd828 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx
+++ b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx
@@ -228,4 +228,13 @@ Valid formats are:
An argument is required to be supplied to the '{0}' parameter.
+
+ The parameter "-File" is required by policy.
+
+
+ The parameter "-NoExit" is disallowed by policy.
+
+
+ Server mode is disallowed by policy.
+
diff --git a/src/System.Management.Automation/security/wldpNativeMethods.cs b/src/System.Management.Automation/security/wldpNativeMethods.cs
index ab49f927614..fa104fc0b6b 100644
--- a/src/System.Management.Automation/security/wldpNativeMethods.cs
+++ b/src/System.Management.Automation/security/wldpNativeMethods.cs
@@ -69,12 +69,65 @@ public enum SystemEnforcementMode
/// Support class for dealing with the Windows Lockdown Policy,
/// Device Guard, and Constrained PowerShell.
///
- public sealed class SystemPolicy
+ public sealed partial class SystemPolicy
{
private SystemPolicy()
{
}
+ // The S in PowerShell must be lower case to match the manifest.
+ private const string AppManifestId = "Powershell";
+
+ private static bool? s_isFileOnlyEntryEnabled;
+
+ ///
+ /// Determines if the WLDP setting "FileOnlyEntry" is enabled.
+ ///
+ internal static bool IsFileOnlyEntryEnabled()
+ {
+ if (s_isFileOnlyEntryEnabled.HasValue)
+ {
+ return s_isFileOnlyEntryEnabled.Value;
+ }
+
+ const string SettingName = "FileOnlyEntry";
+ s_isFileOnlyEntryEnabled = TestBooleanWldpSetting(SettingName);
+ return s_isFileOnlyEntryEnabled.Value;
+ }
+
+ private static bool TestBooleanWldpSetting(string settingName)
+ {
+ int hr = WldpNativeMethods.WldpGetApplicationSettingBoolean(
+ AppManifestId,
+ settingName,
+ out bool result);
+
+ PSEtwLog.LogWDACQueryEvent(
+ "WldpGetApplicationSettingBoolean",
+ settingName,
+ hr,
+ result ? 1 : 0);
+
+ if (hr is not 0)
+ {
+ result = false;
+ }
+
+ if (!result)
+ {
+ string debugValue = Environment.GetEnvironmentVariable(
+ $"__PSLockdownPolicy_{settingName}",
+ EnvironmentVariableTarget.Machine);
+
+ if (debugValue is "1")
+ {
+ result = true;
+ }
+ }
+
+ return result;
+ }
+
///
/// Writes to PowerShell WDAC Audit mode ETW log.
///
@@ -811,8 +864,16 @@ internal enum WLDP_EXECUTION_POLICY
///
/// Native methods for dealing with the lockdown policy.
///
- internal static class WldpNativeMethods
+ internal static partial class WldpNativeMethods
{
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ [LibraryImport("wldp.dll", StringMarshalling = StringMarshalling.Utf16)]
+ internal static partial int WldpGetApplicationSettingBoolean(
+ string id,
+ string setting,
+ [MarshalAs(UnmanagedType.Bool)]
+ out bool result);
+
///
/// Returns a WLDP_EXECUTION_POLICY enum value indicating if and how a script file
/// should be executed.
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1
new file mode 100644
index 00000000000..c1b61a59a12
--- /dev/null
+++ b/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1
@@ -0,0 +1,79 @@
+using namespace System.Diagnostics.CodeAnalysis
+
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+##
+## ----------
+## Test Note:
+## ----------
+## Since these tests change system state (the "FileOnlyEntry" setting)
+## they will all use try/finally blocks instead of Pester AfterEach/AfterAll to
+## ensure system state is restored.
+##
+
+Import-Module HelpersSecurity
+
+try
+{
+ $defaultParamValues = $PSDefaultParameterValues.Clone()
+ $PSDefaultParameterValues["it:Skip"] = !$IsWindows
+
+ Describe "File Only Entry throws for interactive or non-file scenarios" -Tags 'CI','RequireAdminOnWindows' {
+
+ BeforeAll {
+ function MakeTestCase {
+ param([Parameter(ValueFromRemainingArguments)] [string[]] $ArgumentList)
+ end {
+ @{
+ Arguments = $ArgumentList
+ TestName = 'With args "{0}"' -f ($ArgumentList -join ' ')
+ }
+ }
+ }
+
+ [SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'PwshParameterTestCases')]
+ $PwshParameterTestCases = @(
+ MakeTestCase -NoExit -Command Get-ChildItem
+ MakeTestCase -Command Get-ChildItem
+ # File validation should come after `FileOnlyEntry` check, so
+ # this file should not need to exist for us to get the error
+ # we expect.
+ MakeTestCase -NoExit -File this_file_does_not_exist.ps1
+ MakeTestCase -File -
+ MakeTestCase -EncodedCommand RwBlAHQALQBDAGgAaQBsAGQASQB0AGUAbQA= <# < Get-ChildItem #>
+ MakeTestCase -CommandWithArgs Get-ChildItem
+ MakeTestCase
+ )
+ }
+
+ It "" -TestCases $PwshParameterTestCases {
+ param($Arguments)
+
+ $results = $null
+ try {
+ Invoke-LanguageModeTestingSupportCmdlet -SetFileOnlyEntry
+ if ($Arguments -and $Arguments[-1] -eq '-') {
+ $results = 'Get-ChildItem' | & "$PSHOME\pwsh.exe" @Arguments 2>&1
+ } else {
+ $results = & "$PSHOME\pwsh.exe" @Arguments 2>&1
+ }
+ } finally {
+ Invoke-LanguageModeTestingSupportCmdlet -RevertFileOnlyEntry
+ }
+
+ if ($Arguments -contains '-NoExit') {
+ $results.Exception.Message | Should -Be 'The parameter "-NoExit" is disallowed by policy.'
+ } else {
+ $results.Exception.Message | Should -Be 'The parameter "-File" is required by policy.'
+ }
+ }
+ }
+}
+finally
+{
+ if ($null -ne $defaultParamValues)
+ {
+ $Global:PSDefaultParameterValues = $defaultParamValues
+ }
+}
diff --git a/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 b/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1
index 5460b16b8ae..4f9a3d3357f 100644
--- a/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1
+++ b/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1
@@ -27,6 +27,12 @@ if ($IsWindows)
[Parameter()]
public SwitchParameter RevertLockdownMode { get; set; }
+ [Parameter()]
+ public SwitchParameter SetFileOnlyEntry { get; set; }
+
+ [Parameter()]
+ public SwitchParameter RevertFileOnlyEntry { get; set; }
+
protected override void BeginProcessing()
{
if (EnableFullLanguageMode)
@@ -43,6 +49,16 @@ if ($IsWindows)
{
Environment.SetEnvironmentVariable("__PSLockdownPolicy", null, EnvironmentVariableTarget.Machine);
}
+
+ if (SetFileOnlyEntry)
+ {
+ Environment.SetEnvironmentVariable("__PSLockdownPolicy_FileOnlyEntry", "1", EnvironmentVariableTarget.Machine);
+ }
+
+ if (RevertFileOnlyEntry)
+ {
+ Environment.SetEnvironmentVariable("__PSLockdownPolicy_FileOnlyEntry", null, EnvironmentVariableTarget.Machine);
+ }
}
}
'@