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); + } } } '@