From ea7acb2d267c3a76cc3274f3113cf4065874af1e Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Wed, 22 Oct 2025 13:39:16 -0400 Subject: [PATCH 01/14] Add option for disabling fileless entry --- .../host/msh/CommandLineParameterParser.cs | 29 ++++++++++ .../CommandLineParameterParserStrings.resx | 3 ++ .../security/wldpNativeMethods.cs | 54 ++++++++++++++++++- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 0fb85e740f4..477ab546775 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; @@ -882,6 +883,11 @@ private void ParseHelper(string[] args) { if (args.Length == 0) { + if (SystemPolicy.IsFilelessEntryDisabled()) + { + SetCommandLineError(CommandLineParameterParserStrings.FilelessEntryNotAllowed); + } + return; } @@ -927,6 +933,12 @@ private void ParseHelper(string[] args) _noExit = true; noexitSeen = true; ParametersUsed |= ParameterBitmap.NoExit; + + if (SystemPolicy.IsFilelessEntryDisabled()) + { + SetCommandLineError(CommandLineParameterParserStrings.FilelessEntryNotAllowed); + break; + } } else if (MatchSwitch(switchKey, "noprofile", "nop")) { @@ -1263,6 +1275,11 @@ private void ParseHelper(string[] args) } } + if (_error is null && (ParametersUsed & ParameterBitmap.File) is 0 && SystemPolicy.IsFilelessEntryDisabled()) + { + SetCommandLineError(CommandLineParameterParserStrings.FilelessEntryNotAllowed); + } + Dbg.Assert( ((_exitCode == ConsoleHost.ExitCodeBadCommandLineParameter) && _abortStartup) || (_exitCode == ConsoleHost.ExitCodeSuccess), @@ -1360,6 +1377,12 @@ private bool ParseFile(string[] args, ref int i, bool noexitSeen) // Process interactive input... if (args[i] == "-") { + if (SystemPolicy.IsFilelessEntryDisabled()) + { + SetCommandLineError(CommandLineParameterParserStrings.FilelessEntryNotAllowed); + return false; + } + // the arg to -file is -, which is secret code for "read the commands from stdin with prompts" _explicitReadCommandsFromStdin = true; @@ -1492,6 +1515,12 @@ static object ConvertToBoolIfPossible(string arg) private bool ParseCommand(string[] args, ref int i, bool noexitSeen, bool isEncoded) { + if (SystemPolicy.IsFilelessEntryDisabled()) + { + SetCommandLineError(CommandLineParameterParserStrings.FilelessEntryNotAllowed); + return false; + } + if (_commandLineCommand != null) { // we've already set the command, so squawk diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx index 33445ceebd2..85740fd558a 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx @@ -228,4 +228,7 @@ Valid formats are: An argument is required to be supplied to the '{0}' parameter. + + Fileless execution is not allowed by policy. Please use the -File parameter. + diff --git a/src/System.Management.Automation/security/wldpNativeMethods.cs b/src/System.Management.Automation/security/wldpNativeMethods.cs index ab49f927614..96b187e48e8 100644 --- a/src/System.Management.Automation/security/wldpNativeMethods.cs +++ b/src/System.Management.Automation/security/wldpNativeMethods.cs @@ -69,12 +69,54 @@ 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() { } + private static bool? s_isFilelessEntryDisabled; + + internal static bool IsFilelessEntryDisabled() + { + if (s_isFilelessEntryDisabled.HasValue) + { + return s_isFilelessEntryDisabled.Value; + } + + const string SettingName = "DisableFilelessEntry"; + int hr = WldpNativeMethods.WldpGetApplicationSettingBoolean( + "Powershell", + SettingName, + out bool disabled); + + PSEtwLog.LogWDACQueryEvent( + "WldpGetApplicationSettingBoolean", + SettingName, + hr, + disabled ? 1 : 0); + + const int NOT_FOUND = unchecked((int)0x80070490); + if (hr is NOT_FOUND) + { + disabled = false; + } + + if (!disabled) + { + string result = Environment.GetEnvironmentVariable( + "__PSLockdownPolicy_DisableFileless", + EnvironmentVariableTarget.Machine); + if (result is "1") + { + disabled = true; + } + } + + s_isFilelessEntryDisabled = disabled; + return disabled; + } + /// /// Writes to PowerShell WDAC Audit mode ETW log. /// @@ -811,8 +853,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. From 8c5785261bc4097d681b9c6613b407f07cdcebfa Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Tue, 28 Oct 2025 15:50:39 -0400 Subject: [PATCH 02/14] Update feature name and add escape hatch --- .../host/msh/CommandLineParameterParser.cs | 20 +++--- .../CommandLineParameterParserStrings.resx | 4 +- .../security/wldpNativeMethods.cs | 63 +++++++++++++------ 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 477ab546775..fbfe0bfbf9c 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -883,9 +883,9 @@ private void ParseHelper(string[] args) { if (args.Length == 0) { - if (SystemPolicy.IsFilelessEntryDisabled()) + if (SystemPolicy.IsFileOnlyEntryEnabled()) { - SetCommandLineError(CommandLineParameterParserStrings.FilelessEntryNotAllowed); + SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); } return; @@ -934,9 +934,9 @@ private void ParseHelper(string[] args) noexitSeen = true; ParametersUsed |= ParameterBitmap.NoExit; - if (SystemPolicy.IsFilelessEntryDisabled()) + if (SystemPolicy.IsFileOnlyEntryEnabled() && !SystemPolicy.IsNoExitAllowed()) { - SetCommandLineError(CommandLineParameterParserStrings.FilelessEntryNotAllowed); + SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); break; } } @@ -1275,9 +1275,9 @@ private void ParseHelper(string[] args) } } - if (_error is null && (ParametersUsed & ParameterBitmap.File) is 0 && SystemPolicy.IsFilelessEntryDisabled()) + if (_error is null && (ParametersUsed & ParameterBitmap.File) is 0 && SystemPolicy.IsFileOnlyEntryEnabled()) { - SetCommandLineError(CommandLineParameterParserStrings.FilelessEntryNotAllowed); + SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); } Dbg.Assert( @@ -1377,9 +1377,9 @@ private bool ParseFile(string[] args, ref int i, bool noexitSeen) // Process interactive input... if (args[i] == "-") { - if (SystemPolicy.IsFilelessEntryDisabled()) + if (SystemPolicy.IsFileOnlyEntryEnabled()) { - SetCommandLineError(CommandLineParameterParserStrings.FilelessEntryNotAllowed); + SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); return false; } @@ -1515,9 +1515,9 @@ static object ConvertToBoolIfPossible(string arg) private bool ParseCommand(string[] args, ref int i, bool noexitSeen, bool isEncoded) { - if (SystemPolicy.IsFilelessEntryDisabled()) + if (SystemPolicy.IsFileOnlyEntryEnabled()) { - SetCommandLineError(CommandLineParameterParserStrings.FilelessEntryNotAllowed); + SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); return false; } diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx index 85740fd558a..a7155c3a0a6 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx @@ -228,7 +228,7 @@ Valid formats are: An argument is required to be supplied to the '{0}' parameter. - - Fileless execution is not allowed by policy. Please use the -File parameter. + + The parameter "-File" is required by policy. diff --git a/src/System.Management.Automation/security/wldpNativeMethods.cs b/src/System.Management.Automation/security/wldpNativeMethods.cs index 96b187e48e8..ab50e99e4cc 100644 --- a/src/System.Management.Automation/security/wldpNativeMethods.cs +++ b/src/System.Management.Automation/security/wldpNativeMethods.cs @@ -75,46 +75,69 @@ private SystemPolicy() { } - private static bool? s_isFilelessEntryDisabled; + // The S in PowerShell must be lower case to match the manifest. + private const string AppManifestId = "Powershell"; - internal static bool IsFilelessEntryDisabled() + private static bool? s_isFileOnlyEntryEnabled; + + internal static bool IsFileOnlyEntryEnabled() + { + if (s_isFileOnlyEntryEnabled.HasValue) + { + return s_isFileOnlyEntryEnabled.Value; + } + + const string SettingName = "EnableFileOnlyEntry"; + s_isFileOnlyEntryEnabled = TestBooleanWldpSetting(SettingName); + return s_isFileOnlyEntryEnabled.Value; + } + + private static bool? s_allowNoExit; + + internal static bool IsNoExitAllowed() { - if (s_isFilelessEntryDisabled.HasValue) + if (s_allowNoExit.HasValue) { - return s_isFilelessEntryDisabled.Value; + return s_allowNoExit.Value; } - const string SettingName = "DisableFilelessEntry"; + const string SettingName = "AllowNoExit"; + s_allowNoExit = TestBooleanWldpSetting(SettingName); + return s_allowNoExit.Value; + } + + private static bool TestBooleanWldpSetting(string settingName) + { int hr = WldpNativeMethods.WldpGetApplicationSettingBoolean( - "Powershell", - SettingName, - out bool disabled); + AppManifestId, + settingName, + out bool result); PSEtwLog.LogWDACQueryEvent( "WldpGetApplicationSettingBoolean", - SettingName, + settingName, hr, - disabled ? 1 : 0); + result ? 1 : 0); - const int NOT_FOUND = unchecked((int)0x80070490); - if (hr is NOT_FOUND) + if (hr is not 0) { - disabled = false; + result = false; } - if (!disabled) + if (!result) { - string result = Environment.GetEnvironmentVariable( - "__PSLockdownPolicy_DisableFileless", + string debugValue = Environment.GetEnvironmentVariable( + $"__PSLockdownPolicy_{settingName}", EnvironmentVariableTarget.Machine); - if (result is "1") + + if (debugValue is "1") { - disabled = true; + result = true; } } - s_isFilelessEntryDisabled = disabled; - return disabled; + s_isFileOnlyEntryEnabled = result; + return result; } /// From 5824e1e09fb6c5d2ac3e1046a87634b45d9f9d6f Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Tue, 27 Jan 2026 14:59:36 -0500 Subject: [PATCH 03/14] Add tests --- .../host/msh/CommandLineParameterParser.cs | 2 +- .../security/wldpNativeMethods.cs | 17 +---- .../FileOnlyEntry.Tests.ps1 | 73 +++++++++++++++++++ .../HelpersSecurity/HelpersSecurity.psm1 | 16 ++++ 4 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index fbfe0bfbf9c..508d3088b49 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -934,7 +934,7 @@ private void ParseHelper(string[] args) noexitSeen = true; ParametersUsed |= ParameterBitmap.NoExit; - if (SystemPolicy.IsFileOnlyEntryEnabled() && !SystemPolicy.IsNoExitAllowed()) + if (SystemPolicy.IsFileOnlyEntryEnabled()) { SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); break; diff --git a/src/System.Management.Automation/security/wldpNativeMethods.cs b/src/System.Management.Automation/security/wldpNativeMethods.cs index ab50e99e4cc..9c633e271c3 100644 --- a/src/System.Management.Automation/security/wldpNativeMethods.cs +++ b/src/System.Management.Automation/security/wldpNativeMethods.cs @@ -87,25 +87,11 @@ internal static bool IsFileOnlyEntryEnabled() return s_isFileOnlyEntryEnabled.Value; } - const string SettingName = "EnableFileOnlyEntry"; + const string SettingName = "FileOnlyEntry"; s_isFileOnlyEntryEnabled = TestBooleanWldpSetting(SettingName); return s_isFileOnlyEntryEnabled.Value; } - private static bool? s_allowNoExit; - - internal static bool IsNoExitAllowed() - { - if (s_allowNoExit.HasValue) - { - return s_allowNoExit.Value; - } - - const string SettingName = "AllowNoExit"; - s_allowNoExit = TestBooleanWldpSetting(SettingName); - return s_allowNoExit.Value; - } - private static bool TestBooleanWldpSetting(string settingName) { int hr = WldpNativeMethods.WldpGetApplicationSettingBoolean( @@ -136,7 +122,6 @@ private static bool TestBooleanWldpSetting(string settingName) } } - s_isFileOnlyEntryEnabled = result; return result; } 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..335423e5794 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 @@ -0,0 +1,73 @@ +using namespace System.Diagnostics.CodeAnalysis + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +## +## ---------- +## Test Note: +## ---------- +## Since these tests change session and system state (constrained language and system lockdown) +## they will all use try/finally blocks instead of Pester AfterEach/AfterAll to ensure session +## and system state is restored. +## Pester AfterEach, AfterAll is not reliable when the session is constrained language or locked down. +## + +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 - + ) + } + + It "" -TestCases $PwshParameterTestCases { + param($Arguments) + + $results = $null + try { + Invoke-LanguageModeTestingSupportCmdlet -SetFileOnlyEntry + if ($Arguments[-1] -eq '-') { + $results = 'Get-ChildItem' | & "$PSHOME\pwsh.exe" @Arguments 2>&1 + } else { + $results = & "$PSHOME\pwsh.exe" @Arguments 2>&1 + } + } finally { + Invoke-LanguageModeTestingSupportCmdlet -RevertFileOnlyEntry + } + + $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); + } } } '@ From e5efb833f813aeb54b53ef674ab1db7d233316d6 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Thu, 5 Feb 2026 12:06:08 -0500 Subject: [PATCH 04/14] Add method documentation --- src/System.Management.Automation/security/wldpNativeMethods.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/System.Management.Automation/security/wldpNativeMethods.cs b/src/System.Management.Automation/security/wldpNativeMethods.cs index 9c633e271c3..fa104fc0b6b 100644 --- a/src/System.Management.Automation/security/wldpNativeMethods.cs +++ b/src/System.Management.Automation/security/wldpNativeMethods.cs @@ -80,6 +80,9 @@ private SystemPolicy() private static bool? s_isFileOnlyEntryEnabled; + /// + /// Determines if the WLDP setting "FileOnlyEntry" is enabled. + /// internal static bool IsFileOnlyEntryEnabled() { if (s_isFileOnlyEntryEnabled.HasValue) From 71a789180e236a69996333a006a735f186931ab1 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Thu, 5 Feb 2026 12:06:34 -0500 Subject: [PATCH 05/14] Fix non-windows build error --- .../host/msh/CommandLineParameterParser.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 508d3088b49..cb288ca800c 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -879,11 +879,23 @@ internal void Parse(string[] args) ParseHelper(args); } + private static bool IsFileOnlyEntryEnabled + { + get + { +#if UNIX + return false; +#else + return SystemPolicy.IsFileOnlyEntryEnabled(); +#endif + } + } + private void ParseHelper(string[] args) { if (args.Length == 0) { - if (SystemPolicy.IsFileOnlyEntryEnabled()) + if (IsFileOnlyEntryEnabled) { SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); } @@ -934,7 +946,7 @@ private void ParseHelper(string[] args) noexitSeen = true; ParametersUsed |= ParameterBitmap.NoExit; - if (SystemPolicy.IsFileOnlyEntryEnabled()) + if (IsFileOnlyEntryEnabled) { SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); break; @@ -1275,7 +1287,7 @@ private void ParseHelper(string[] args) } } - if (_error is null && (ParametersUsed & ParameterBitmap.File) is 0 && SystemPolicy.IsFileOnlyEntryEnabled()) + if (_error is null && (ParametersUsed & ParameterBitmap.File) is 0 && IsFileOnlyEntryEnabled) { SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); } @@ -1377,7 +1389,7 @@ private bool ParseFile(string[] args, ref int i, bool noexitSeen) // Process interactive input... if (args[i] == "-") { - if (SystemPolicy.IsFileOnlyEntryEnabled()) + if (IsFileOnlyEntryEnabled) { SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); return false; @@ -1515,7 +1527,7 @@ static object ConvertToBoolIfPossible(string arg) private bool ParseCommand(string[] args, ref int i, bool noexitSeen, bool isEncoded) { - if (SystemPolicy.IsFileOnlyEntryEnabled()) + if (IsFileOnlyEntryEnabled) { SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); return false; From d6284308cfa328fc82e67640f9b09096e893da76 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Thu, 5 Feb 2026 12:26:44 -0500 Subject: [PATCH 06/14] Fix `-help` and `-version` --- .../host/msh/CommandLineParameterParser.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index cb288ca800c..81cb901ae27 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -1287,7 +1287,11 @@ private void ParseHelper(string[] args) } } - if (_error is null && (ParametersUsed & ParameterBitmap.File) is 0 && IsFileOnlyEntryEnabled) + if (_error is null + && !_showVersion + && !_showHelp + && (ParametersUsed & ParameterBitmap.File) is 0 + && IsFileOnlyEntryEnabled) { SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); } From 8ecbbee35a4cda67c32acf5399541abf8f042d19 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Thu, 5 Feb 2026 12:32:39 -0500 Subject: [PATCH 07/14] Add more test cases --- .../FileOnlyEntry.Tests.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 index 335423e5794..a4b655ce09f 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 @@ -7,10 +7,9 @@ using namespace System.Diagnostics.CodeAnalysis ## ---------- ## Test Note: ## ---------- -## Since these tests change session and system state (constrained language and system lockdown) -## they will all use try/finally blocks instead of Pester AfterEach/AfterAll to ensure session -## and system state is restored. -## Pester AfterEach, AfterAll is not reliable when the session is constrained language or locked down. +## 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 @@ -42,6 +41,9 @@ try # we expect. MakeTestCase -NoExit -File this_file_does_not_exist.ps1 MakeTestCase -File - + MakeTestCase -EncodedCommand RwBlAHQALQBDAGgAaQBsAGQASQB0AGUAbQA= <# < Get-ChildItem #> + MakeTestCase -CommandWithArgs Get-ChildItem + MakeTestCase ) } From 5458a66bc2dc01d5d3e3bf0e47fce0daf0007723 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Thu, 12 Feb 2026 12:53:32 -0500 Subject: [PATCH 08/14] Fix argless test --- .../Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 index a4b655ce09f..d82df64352c 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 @@ -53,7 +53,7 @@ try $results = $null try { Invoke-LanguageModeTestingSupportCmdlet -SetFileOnlyEntry - if ($Arguments[-1] -eq '-') { + if ($Arguments -and $Arguments[-1] -eq '-') { $results = 'Get-ChildItem' | & "$PSHOME\pwsh.exe" @Arguments 2>&1 } else { $results = & "$PSHOME\pwsh.exe" @Arguments 2>&1 From e962541d52528982f187d7e6820d8d8bd9ecf221 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Wed, 27 May 2026 14:18:24 -0400 Subject: [PATCH 09/14] Add message specifying `-NoExit` is disallowed --- .../host/msh/CommandLineParameterParser.cs | 2 +- .../resources/CommandLineParameterParserStrings.resx | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 81cb901ae27..1e6c4162c31 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -948,7 +948,7 @@ private void ParseHelper(string[] args) if (IsFileOnlyEntryEnabled) { - SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); + SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryNoExitDisabled); break; } } diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx index a7155c3a0a6..3e6fdba7e9f 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx @@ -231,4 +231,7 @@ Valid formats are: The parameter "-File" is required by policy. + + The parameter "-NoExit" is disallowed by policy. + From 4706f4ab010a5740669a22bfc5a4ac9660addb9b Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Wed, 27 May 2026 14:23:51 -0400 Subject: [PATCH 10/14] use `.HasFlag` instead of bitwise operators --- .../host/msh/CommandLineParameterParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 1e6c4162c31..faac48d944e 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -1290,7 +1290,7 @@ private void ParseHelper(string[] args) if (_error is null && !_showVersion && !_showHelp - && (ParametersUsed & ParameterBitmap.File) is 0 + && !ParametersUsed.HasFlag(ParameterBitmap.File) && IsFileOnlyEntryEnabled) { SetCommandLineError(CommandLineParameterParserStrings.FileOnlyEntryRequired); From 87a5f27544881f536e40f1eee9730cf4a4497642 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Fri, 29 May 2026 13:32:23 -0400 Subject: [PATCH 11/14] Disallow server mode for FileOnlyEntry --- .../host/msh/CommandLineParameterParser.cs | 2 +- .../host/msh/ConsoleHost.cs | 8 ++++++++ .../resources/ConsoleHostStrings.resx | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index faac48d944e..61e4cc91020 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -879,7 +879,7 @@ internal void Parse(string[] args) ParseHelper(args); } - private static bool IsFileOnlyEntryEnabled + internal static bool IsFileOnlyEntryEnabled { get { diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index 2d9902d176a..7a84af35313 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs @@ -223,6 +223,14 @@ internal static int Start( return ExitCodeBadCommandLineParameter; } + if (serverModeCount is 1 && CommandLineParameterParser.IsFileOnlyEntryEnabled) + { + s_tracer.TraceError("Server mode cannot be specified when FileOnlyEntry policy is in place."); + s_theConsoleHost?.ui.WriteErrorLine(ConsoleHostStrings.FileOnlyEntryServerMode); + + return ExitCodeBadCommandLineParameter; + } + #if !UNIX TaskbarJumpList.CreateRunAsAdministratorJumpList(); #endif diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx index 9bc06e0d42f..69824547359 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx @@ -176,6 +176,9 @@ The current session does not support debugging; execution will continue. More than one server mode parameter was specified. Server mode parameters must be used exclusively. + + Server mode is disallowed by policy. + Loading personal and system profiles took {0}ms. From 53eb12ebcd1d7d1e7331f38bd99474c0e19f3656 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Fri, 29 May 2026 13:35:01 -0400 Subject: [PATCH 12/14] Clean up the readability of `ConsoleHost.Start` --- .../host/msh/ConsoleHost.cs | 77 +++++++++---------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index 7a84af35313..0e542513074 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. @@ -245,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"); @@ -257,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) { @@ -292,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(); - PSHost.IsStdOutputRedirected = Console.IsOutputRedirected; + s_theConsoleHost.BindBreakHandler(); + PSHost.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 { @@ -357,11 +357,6 @@ internal static int Start( } #pragma warning restore IDE0031 } - - unchecked - { - return (int)exitCode; - } } internal static void ParseCommandLine(string[] args) From 894e5d31ef2f557dc455bee55e91608a7d807841 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 1 Jun 2026 12:17:06 -0400 Subject: [PATCH 13/14] Change where the server mode check occurs Both the generic "file must be specified" and "server mode disallowed" error messages were being written which was confusing. --- .../host/msh/CommandLineParameterParser.cs | 25 +++++++++++++++++++ .../host/msh/ConsoleHost.cs | 4 +-- .../CommandLineParameterParserStrings.resx | 3 +++ .../resources/ConsoleHostStrings.resx | 3 --- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 61e4cc91020..0546dc527ee 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -972,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")) @@ -979,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")) @@ -986,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")) { diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index 0e542513074..3b4ad7fec76 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs @@ -223,9 +223,9 @@ internal static int Start( 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."); - s_theConsoleHost?.ui.WriteErrorLine(ConsoleHostStrings.FileOnlyEntryServerMode); - return ExitCodeBadCommandLineParameter; } diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx index 3e6fdba7e9f..77cf184afe6 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx @@ -234,4 +234,7 @@ Valid formats are: The parameter "-NoExit" is disallowed by policy. + + Server mode is disallowed by policy. + diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx index 69824547359..9bc06e0d42f 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ConsoleHostStrings.resx @@ -176,9 +176,6 @@ The current session does not support debugging; execution will continue. More than one server mode parameter was specified. Server mode parameters must be used exclusively. - - Server mode is disallowed by policy. - Loading personal and system profiles took {0}ms. From fe6651165cfec9148c98d77685806419c642d60f Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Wed, 5 Aug 2026 13:38:38 -0400 Subject: [PATCH 14/14] Fix test for new message --- .../Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 index d82df64352c..c1b61a59a12 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/FileOnlyEntry.Tests.ps1 @@ -62,7 +62,11 @@ try Invoke-LanguageModeTestingSupportCmdlet -RevertFileOnlyEntry } - $results.Exception.Message | Should -Be 'The parameter "-File" is required by policy.' + 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.' + } } } }