From 83350590834e26ecbcb5b16646a68efcb3028ffa Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Wed, 3 Oct 2018 10:02:54 -0700 Subject: [PATCH 01/13] Added ps1 file import restriction. Refactored InvokeLanguageModeTestingSupportCmdlet to HelpersSecurity module --- .../engine/Modules/ModuleCmdletBase.cs | 9 ++ .../resources/Modules.resx | 3 + .../ConstrainedLanguageRestriction.Tests.ps1 | 85 +----------------- .../ConstrainedLanguageValidation.Tests.ps1 | 65 ++++++++++++++ .../HelpersSecurity/HelpersSecurity.psd1 | Bin 0 -> 756 bytes .../HelpersSecurity/HelpersSecurity.psm1 | Bin 0 -> 5462 bytes 6 files changed, 78 insertions(+), 84 deletions(-) create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageValidation.Tests.ps1 create mode 100644 test/tools/Modules/HelpersSecurity/HelpersSecurity.psd1 create mode 100644 test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 340b95c49c4..79065672b80 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -5379,6 +5379,15 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str } PSModuleInfo module = null; + // Block ps1 files from being imported in constrained language. + if (Context.LanguageMode == PSLanguageMode.ConstrainedLanguage && ext.Equals(StringLiterals.PowerShellScriptFileExtension, StringComparison.OrdinalIgnoreCase)) + { + InvalidOperationException invalidOp = new InvalidOperationException(Modules.ImportPSFileNotAllowedInConstrainedLanguage); + ErrorRecord er = new ErrorRecord(invalidOp, "Modules_ImportPSFileNotAllowedInConstrainedLanguage", + ErrorCategory.PermissionDenied, null); + ThrowTerminatingError(er); + } + // If MinimumVersion/RequiredVersion/MaximumVersion has been specified, then only try to process manifest modules... if (BaseMinimumVersion != null || BaseMaximumVersion != null || BaseRequiredVersion != null || BaseGuid != null) { diff --git a/src/System.Management.Automation/resources/Modules.resx b/src/System.Management.Automation/resources/Modules.resx index d351e756e3a..97597cc827d 100644 --- a/src/System.Management.Automation/resources/Modules.resx +++ b/src/System.Management.Automation/resources/Modules.resx @@ -618,4 +618,7 @@ The -SkipEditionCheck switch parameter cannot be used without the -ListAvailable switch parameter. + + Importing *.ps1 files as modules is not allowed in ConstrainedLanguage mode. + diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 index 25421dc978f..ffd388b6f73 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 @@ -11,90 +11,7 @@ ## Pester AfterEach, AfterAll is not reliable when the session is constrained language or locked down. ## -if ($IsWindows) -{ - $code = @' - - #region Using directives - - using System; - using System.Globalization; - using System.Reflection; - using System.Collections; - using System.Collections.Generic; - using System.IO; - using System.Security; - using System.Runtime.InteropServices; - using System.Threading; - using System.Management.Automation; - - #endregion - - /// Adds a new type to the Application Domain - [Cmdlet("Invoke", "LanguageModeTestingSupportCmdlet")] - public sealed class InvokeLanguageModeTestingSupportCmdlet : PSCmdlet - { - [Parameter()] - public SwitchParameter EnableFullLanguageMode - { - get { return enableFullLanguageMode; } - set { enableFullLanguageMode = value; } - } - private SwitchParameter enableFullLanguageMode; - - [Parameter()] - public SwitchParameter SetLockdownMode - { - get { return setLockdownMode; } - set { setLockdownMode = value; } - } - private SwitchParameter setLockdownMode; - - [Parameter()] - public SwitchParameter RevertLockdownMode - { - get { return revertLockdownMode; } - set { revertLockdownMode = value; } - } - private SwitchParameter revertLockdownMode; - - protected override void BeginProcessing() - { - if (enableFullLanguageMode) - { - SessionState.LanguageMode = PSLanguageMode.FullLanguage; - } - - if (setLockdownMode) - { - Environment.SetEnvironmentVariable("__PSLockdownPolicy", "0x80000007", EnvironmentVariableTarget.Machine); - } - - if (revertLockdownMode) - { - Environment.SetEnvironmentVariable("__PSLockdownPolicy", null, EnvironmentVariableTarget.Machine); - } - } - } -'@ - - if (-not (Get-Command Invoke-LanguageModeTestingSupportCmdlet -ErrorAction Ignore)) - { - $moduleName = Get-RandomFileName - $moduleDirectory = join-path $TestDrive\Modules $moduleName - if (-not (Test-Path $moduleDirectory)) - { - $null = New-Item -ItemType Directory $moduleDirectory -Force - } - - try - { - Add-Type -TypeDefinition $code -OutputAssembly $moduleDirectory\TestCmdletForConstrainedLanguage.dll -ErrorAction Ignore - } catch {} - - Import-Module -Name $moduleDirectory\TestCmdletForConstrainedLanguage.dll - } -} # end if ($IsWindows) +Import-Module HelpersSecurity try { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageValidation.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageValidation.Tests.ps1 new file mode 100644 index 00000000000..09ec771fe13 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageValidation.Tests.ps1 @@ -0,0 +1,65 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# 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 "Importing PowerShell script files are not allowed in ConstrainedLanguage" -Tags 'CI','RequireAdminOnWindows' { + + BeforeAll { + + $scriptFileName = (Get-RandomFileName) + ".ps1" + $scriptFilePath = Join-Path $TestDrive $scriptFileName + '"Hello!"' > $scriptFilePath + } + + It "Verifies that ps1 script file cannot be imported in ConstrainedLanguage mode" { + + $err = $null + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Import-Module -Name $scriptFilePath + throw "No Exception!" + } + catch + { + $err = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } + + $err.FullyQualifiedErrorId | Should -BeExactly "Modules_ImportPSFileNotAllowedInConstrainedLanguage,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Verifies that ps1 script file can be imported in FullLangauge mode" { + + { Import-Module -Name $scriptFilePath } | Should -Not -Throw + } + } + + # End Describe blocks +} +finally +{ + if ($defaultParamValues -ne $null) + { + $Global:PSDefaultParameterValues = $defaultParamValues + } +} diff --git a/test/tools/Modules/HelpersSecurity/HelpersSecurity.psd1 b/test/tools/Modules/HelpersSecurity/HelpersSecurity.psd1 new file mode 100644 index 0000000000000000000000000000000000000000..145dfbd59c3a63e9fa60b4d55935cbd3db715dfe GIT binary patch literal 756 zcma))OHaa35QWd$#Q)HQg}8u#wmudn81c1$g|B^SMVhpfv|=R2zpj4MQiQV5G@Y4y z=ggVMo$s%PcD19191Y}asz_r!E7vPGW2MT_d%EE+)(n~9yGI9_V@Kc?_#Gu+L+)aH zC8J6Ed-^D}1IoTTkSTG4wFpONc>c=Mrh=SIU696HF|IQ`Eo+QmpMofPyK1Xbi@MgM zZq;95Jh^-!xDL`)3#LPjbd2Tdh|>XKZV?fH|i0mBM9Hw}^}glRh_PC)=zu`0NEU}voo2AX9rzYLozI+z)2MD%xh5!Hn literal 0 HcmV?d00001 diff --git a/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 b/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 new file mode 100644 index 0000000000000000000000000000000000000000..4ab90c4a8b90da5bd8c12707755d6d1eb9c2bfca GIT binary patch literal 5462 zcmdUzYi}Dx6o$`dB>uxHvD8LlO}~L?sai;YNc5sOz=yV~Tzlh~#$L-_C$v%jI`F(R zo@Ca$>#k`CLaSY`XU?3?+t@~SY6W9=ZD4!+K4EOYOl}MA z6E`RNIV<$+Gcx9FYF(%w+Zk69aay>48LKl!&v_1^{MN&eY!&seSbAtOuPhmJOZ z^C@rA;n0pN)SintjnPXQcc5F?Z~PaJ+%EXH=VV>FX9AV3Lz}F$FD!+74)qefCF>`i zXUt05g+pcnwd|85-GNI7T(nZFzU6*ocPccyaLK^bBb3-L@RW8V=!d*%cHj^cGKQ6IW$`w3e^iay=#s z6eoQbF^ZkGE1$4t$~|JWG?F%5a&*aW&d7D{dX8~)?>hRSlN{oAJp;ZpU5P!wnkD?i zaSmP&kul^_?)SOntqXpIr7}xzxqJHje$431Md%>XDY5;Pmdcn1NLrT?>-&OSogh=u zR!|Fe9Qx;Qc+1F+eSy3k>WvT?p{+;7=YB?Ykw@e?&Gwj=MyhW`iSG{e7Kwtev%HVWdq*&j0p$DLeS<@wMgt6I6g`cTd^om zmo4kz!7*zFL}QgpVh7mQcSvOR1DGi9o9v7=4btj04zr42?Mj#2%7p?6b0j9bx0ib# z`VY0p1Ud3wkh2YEzXP0uz!{olWN4G5digV~851AMNae@J(B4M3YHkN>|IK(%wJV5# zoQQc#HfT4VuJ+oj)(8JI0=&ldn)yUGZmB?LK->>^o?*r%$QmTItUt;bf{E zVS5jTGx#=@sa{UV%gyy5Q1!4yJFGup_psmB&~D@I<9(~$vuS_oHtt~F2SN7wudnTO z*agaf(&o&*30w~FuFjhB@6_%0{`4oj3b$KVXMY7d#Y@=7b&7h;<*_PP^5^hXPch(3 z7fx6A><|0e?nT#EtGRk@*5w^L`F^kOvRCxoX+v?E5tBECeVbhSe|liLI(O-;ds~Qy z$PT~d^Ns&^hu&CO5oboogcR?pcBy|?XQz%_eWULt)W?_4&S_)TwmriqNH`$Ql^Jt7 zmFvD%l15G+#mL@T9jvcM9W=fEe(;o-)!EwDm|)KlqyCG;gvv4|Z!!y%F(v`SGS7XN z#lDA@#_v|#e@CuzBc)SO%{yhhbJfZ0L#Y#GW-m)$IMj=j`ESys&PxI3)b#;DZj3@z z<8>^9rS+JG*$&qEPQv5)6l*T9_YiB<(aMwI{rIZ4tV&3AZJy-a86n}9_`JSOZUR_#26l)I@ChA2MA!Q#|Eq-|w zDRB?!F;M>LON}h+J1umkSl=zXjQOv9?eeJ(p$};4dw%tm>hGFy-t^Tjy~?NCv^=)R I>+PR^10B&f9{>OV literal 0 HcmV?d00001 From 9752ea5ea093f4f4a1850104531793c5a61a99cc Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Wed, 3 Oct 2018 10:31:39 -0700 Subject: [PATCH 02/13] JEA loop back fix. Debugger running commands in CL mode. --- .../engine/hostifaces/HostUtilities.cs | 1 + .../engine/hostifaces/LocalPipeline.cs | 7 +- .../engine/remoting/client/RunspaceRef.cs | 6 +- .../engine/remoting/client/remoterunspace.cs | 9 + .../ConstrainedLanguageDebugger.Tests.ps1 | 185 ++++++++++++++++++ 5 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 diff --git a/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs b/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs index 906e5443015..ef005f250ee 100644 --- a/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs +++ b/src/System.Management.Automation/engine/hostifaces/HostUtilities.cs @@ -853,6 +853,7 @@ internal static RemoteRunspace CreateConfiguredRunspace( e); } + remoteRunspace.IsConfiguredLoopBack = true; return remoteRunspace; } diff --git a/src/System.Management.Automation/engine/hostifaces/LocalPipeline.cs b/src/System.Management.Automation/engine/hostifaces/LocalPipeline.cs index 3abf7cbf8fc..e3512e379b0 100644 --- a/src/System.Management.Automation/engine/hostifaces/LocalPipeline.cs +++ b/src/System.Management.Automation/engine/hostifaces/LocalPipeline.cs @@ -847,7 +847,12 @@ private PipelineProcessor CreatePipelineProcessor() try { CommandOrigin commandOrigin = command.CommandOrigin; - if (IsNested) + + // Do not set command origin to internal if this is a script debugger originated command (which always + // runs nested commands). This prevents the script debugger command line from seeing private commands. + if (IsNested && + !LocalRunspace.InNestedPrompt && + !((LocalRunspace.Debugger != null) && (LocalRunspace.Debugger.InBreakpoint))) { commandOrigin = CommandOrigin.Internal; } diff --git a/src/System.Management.Automation/engine/remoting/client/RunspaceRef.cs b/src/System.Management.Automation/engine/remoting/client/RunspaceRef.cs index bbaf0903cd6..086c328c8f2 100644 --- a/src/System.Management.Automation/engine/remoting/client/RunspaceRef.cs +++ b/src/System.Management.Automation/engine/remoting/client/RunspaceRef.cs @@ -89,7 +89,11 @@ private PSCommand ParsePsCommandUsingScriptBlock(string line, bool? useLocalScop ExecutionContext context = localRunspace.ExecutionContext; // This is trusted input as long as we're in FullLanguage mode - bool isTrustedInput = (localRunspace.ExecutionContext.LanguageMode == PSLanguageMode.FullLanguage); + // and if we are not in a loopback configuration mode, in which case we always force remote script commands + // to be parsed and evaluated on the remote session (not in the current local session). + RemoteRunspace remoteRunspace = _runspaceRef.Value as RemoteRunspace; + bool isConfiguredLoopback = (remoteRunspace != null) ? remoteRunspace.IsConfiguredLoopBack : false; + bool isTrustedInput = !isConfiguredLoopback && (localRunspace.ExecutionContext.LanguageMode == PSLanguageMode.FullLanguage); // Create PowerShell from ScriptBlock. ScriptBlock scriptBlock = ScriptBlock.Create(context, line); diff --git a/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs b/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs index ed892688b38..23a988cba8c 100644 --- a/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs +++ b/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs @@ -489,6 +489,15 @@ internal bool CanConnect get { return RunspacePool.RemoteRunspacePoolInternal.AvailableForConnection; } } + /// + /// This is used to indicate a special loopback remote session used for JEA restrictions. + /// + internal bool IsConfiguredLoopBack + { + get; + set; + } + /// /// Debugger /// diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 new file mode 100644 index 00000000000..1258d72f55e --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 @@ -0,0 +1,185 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# 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 "Trusted module on locked down machine should not expose private functions to script debugger command processing" -Tags 'CI','RequireAdminOnWindows' { + + BeforeAll { + + # Debugger test type definition + $debuggerTestTypeDef = @' + using System; + using System.Management.Automation; + using System.Management.Automation.Runspaces; + + namespace TestRunner + { + public class DebuggerTester + { + private Runspace _runspace; + private readonly string _privateFnName; + + [Flags] + public enum TestResults + { + NoResult = 0x0, + DebuggerStopHandled = 0x1, + PrivateFnFound = 0x2 + }; + + public TestResults TestResult + { + private set; + get; + } + + public Exception ScriptException + { + private set; + get; + } + + public DebuggerTester(Runspace runspace, string privateFnName) + { + if (runspace.Debugger == null) + { + throw new PSArgumentException("The provided runspace script debugger cannot be null for test."); + } + + _runspace = runspace; + _privateFnName = privateFnName; + _runspace.Debugger.DebuggerStop += (sender, args) => + { + try + { + // Within the debugger stop handler, make sure trusted private functions are not accessible. + string commandText = string.Format(@"Get-Command ""{0}""", _privateFnName); + PSCommand command = new PSCommand(); + command.AddCommand(new Command(commandText, true)); + PSDataCollection output = new PSDataCollection(); + + _runspace.Debugger.ProcessCommand(command, output); + if ((output.Count > 0) && (output[0].BaseObject is CommandInfo)) + { + TestResult |= TestResults.PrivateFnFound; + } + } + catch (Exception e) + { + ScriptException = e; + System.Console.WriteLine(e.Message); + } + TestResult |= TestResults.DebuggerStopHandled; + }; + } + } + } +'@ + + $modulePath = Join-Path $TestDrive Modules + if (Test-Path -Path $modulePath) + { + try { Remove-Item -Path $modulePath -Recurse -Force -ErrorAction SilentlyContinue } catch { } + } + + # Trusted module + $trustedModuleName = "TrustedModule_System32" + $trustedModuleDirectory = Join-Path $modulePath $trustedModuleName + New-Item -ItemType Directory -Path $trustedModuleDirectory -Force -ErrorAction SilentlyContinue + $trustedModuleFilePath = Join-Path $trustedModuleDirectory "$($trustedModuleName).psm1" + $trustedManifestFilePath = Join-Path $trustedModuleDirectory "$($trustedModuleName).psd1" + @' + function PublicFn { + Write-Output PrivateFn "PublicFn" + } + + function PrivateFn { + param ([string] $msg) + + Write-Output $msg + } +'@ > $trustedModuleFilePath + $modManifest = "@{ ModuleVersion = '1.0'" + ("; RootModule = '{0}'" -f $trustedModuleFilePath) + "; FunctionsToExport = 'PublicFn' }" + $modManifest > $trustedManifestFilePath + + # Create test runspace + [runspace] $runspace = [runspacefactory]::CreateRunspace() + $runspace.Open() + + # Create debugger test object + Add-Type -TypeDefinition $debuggerTestTypeDef + } + + AfterAll { + + if ($runspace -ne $null) { $runspace.Dispose() } + } + + It "Verifies that private trusted module function is not available in script debugger" { + + # Run debugger access test + $debuggerTester = [TestRunner.DebuggerTester]::new($runspace, "PrivateFn") + + # Script to invoke the script debugger so that $debuggerTester can handle + # the debugger stop event and test for access of private functions within the + # script debugger command processor. + $script = @' + Import-Module -Name HelpersSecurity + Import-Module -Name {0} -Force + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Import-Module -Name {1} -Force + Set-PSBreakpoint -Command PublicFn + PublicFn +'@ -f "$languageModuleDirectory\TestCmdletForConstrainedLanguage.dll", $trustedManifestFilePath + + [powershell] $ps = [powershell]::Create() + $ps.Runspace = $runspace + + try + { + $ps.AddScript($script).BeginInvoke() + + # Wait for debugger test result for up to ten seconds + $count = 0 + while (($debuggerTester.TestResult -eq 0) -and ($count++ -lt 40)) + { + Start-Sleep -Milliseconds 250 + } + } + finally + { + # Revert lockdown + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + # Verify that PrivateFn function name is not accessible + $debuggerTester.TestResult | Should -Match "DebuggerStopHandled" + $debuggerTester.TestResult | Should -Not -Match "PrivateFnFound" + $debuggerTester.ScriptException | Should -BeNullOrEmpty + } + } +} +finally +{ + if ($defaultParamValues -ne $null) + { + $Global:PSDefaultParameterValues = $defaultParamValues + } +} From 0c862f134f3282dedde173923274783e38c29182 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Wed, 3 Oct 2018 10:49:47 -0700 Subject: [PATCH 03/13] Support for new AMSI codes. Changed to use AMSI buffer API. Unhandled exception fix. --- .../engine/runtime/CompiledScriptBlock.cs | 12 +++++- .../resources/ParserStrings.resx | 3 ++ .../security/SecuritySupport.cs | 41 ++++++++++++++----- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs b/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs index d19dca4c517..e26001e0d62 100644 --- a/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs +++ b/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs @@ -201,11 +201,21 @@ private void PerformSecurityChecks() // Call the AMSI API to determine if the script block has malicious content var scriptExtent = scriptBlockAst.Extent; - if (AmsiUtils.ScanContent(scriptExtent.Text, scriptExtent.File) == AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_DETECTED) + var amsiResult = AmsiUtils.ScanContent(scriptExtent.Text, scriptExtent.File); + + if (amsiResult == AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_DETECTED) { var parseError = new ParseError(scriptExtent, "ScriptContainedMaliciousContent", ParserStrings.ScriptContainedMaliciousContent); throw new ParseException(new[] { parseError }); } + else if ((amsiResult >= AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_BLOCKED_BY_ADMIN_BEGIN) && + (amsiResult <= AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_BLOCKED_BY_ADMIN_END)) + { + // Certain policies set by an administrator blocked this content on this machine + var parseError = new ParseError(scriptExtent, "ScriptHasAdminBlockedContent", + StringUtil.Format(ParserStrings.ScriptHasAdminBlockedContent, amsiResult)); + throw new ParseException(new[] { parseError }); + } if (ScriptBlock.CheckSuspiciousContent(scriptBlockAst) != null) { diff --git a/src/System.Management.Automation/resources/ParserStrings.resx b/src/System.Management.Automation/resources/ParserStrings.resx index ab3e6cbc08d..b0f50665bfa 100644 --- a/src/System.Management.Automation/resources/ParserStrings.resx +++ b/src/System.Management.Automation/resources/ParserStrings.resx @@ -1488,4 +1488,7 @@ ModuleVersion : Version of module to import. If used, ModuleName must represent Implicit remoting command pipeline has been batched for execution on remote target. + + This script contains content that has been flagged as suspicious through a policy setting and has been blocked with error code {0}. Contact your administrator for more information. + diff --git a/src/System.Management.Automation/security/SecuritySupport.cs b/src/System.Management.Automation/security/SecuritySupport.cs index 92fdb86e9ba..124307db9d9 100644 --- a/src/System.Management.Automation/security/SecuritySupport.cs +++ b/src/System.Management.Automation/security/SecuritySupport.cs @@ -1475,6 +1475,11 @@ public enum ResolutionPurpose internal class AmsiUtils { + private static string GetProcessHostName(string processName) + { + return string.Concat("PowerShell_", processName, ".exe_0.0.0.0"); + } + internal static int Init() { Diagnostics.Assert(s_amsiContext == IntPtr.Zero, "Init should be called just once"); @@ -1492,10 +1497,13 @@ internal static int Init() catch (ComponentModel.Win32Exception) { // This exception can be thrown during thread impersonation (Access Denied for process module access). - // Use command line arguments or process name. - string[] cmdLineArgs = Environment.GetCommandLineArgs(); - string processPath = (cmdLineArgs.Length > 0) ? cmdLineArgs[0] : currentProcess.ProcessName; - hostname = string.Concat("PowerShell_", processPath, ".exe_0.0.0.0"); + hostname = GetProcessHostName(currentProcess.ProcessName); + } + catch (FileNotFoundException) + { + // This exception can occur if the file is renamed or moved to some other folder + // (This has occurred during Exchange set up). + hostname = GetProcessHostName(currentProcess.ProcessName); } AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit; @@ -1589,12 +1597,21 @@ internal static AmsiNativeMethods.AMSI_RESULT WinScanContent(string content, str AmsiNativeMethods.AMSI_RESULT result = AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_CLEAN; - hr = AmsiNativeMethods.AmsiScanString( - s_amsiContext, - content, - sourceMetadata, - s_amsiSession, - ref result); + // Run AMSI content scan + unsafe + { + fixed (char* buffer = content) + { + var buffPtr = new IntPtr(buffer); + hr = AmsiNativeMethods.AmsiScanBuffer( + s_amsiContext, + buffPtr, + (uint)(content.Length * sizeof(char)), + sourceMetadata, + s_amsiSession, + ref result); + } + } if (!Utils.Succeeded(hr)) { @@ -1704,6 +1721,10 @@ internal enum AMSI_RESULT /// AMSI_RESULT_NOT_DETECTED -> 1 AMSI_RESULT_NOT_DETECTED = 1, + /// Certain policies set by administrator blocked this content on this machine + AMSI_RESULT_BLOCKED_BY_ADMIN_BEGIN = 0x4000, + AMSI_RESULT_BLOCKED_BY_ADMIN_END = 0x4fff, + /// AMSI_RESULT_DETECTED -> 32768 AMSI_RESULT_DETECTED = 32768, } From 4c96dec9ab30ca5dcd94e40a4f7e1f16a7d75565 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Wed, 3 Oct 2018 14:16:12 -0700 Subject: [PATCH 04/13] Fixes for module bugs while running in ConstrainedLanguage mode --- .../Modules/ExportModuleMemberCommand.cs | 10 + .../engine/Modules/ImportModuleCommand.cs | 14 +- .../engine/Modules/ModuleCmdletBase.cs | 301 +++- .../engine/Modules/ModuleIntrinsics.cs | 51 + .../engine/Modules/PSModuleInfo.cs | 30 + .../engine/SessionStateFunctionAPIs.cs | 29 +- .../engine/runtime/Operations/MiscOps.cs | 8 + .../resources/Modules.resx | 9 + .../resources/ParserStrings.resx | 3 + .../ConstrainedLanguageModules.Tests.ps1 | 1411 +++++++++++++++++ .../ConstrainedLanguageRestriction.Tests.ps1 | 379 ++++- 11 files changed, 2097 insertions(+), 148 deletions(-) create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageModules.Tests.ps1 diff --git a/src/System.Management.Automation/engine/Modules/ExportModuleMemberCommand.cs b/src/System.Management.Automation/engine/Modules/ExportModuleMemberCommand.cs index c6e2a6216d0..c502b195429 100644 --- a/src/System.Management.Automation/engine/Modules/ExportModuleMemberCommand.cs +++ b/src/System.Management.Automation/engine/Modules/ExportModuleMemberCommand.cs @@ -142,6 +142,16 @@ protected override void ProcessRecord() ThrowTerminatingError(er); } + // Prevent script injection attack by disallowing ExportModuleMemberCommand to export module members across + // language boundaries. This will prevent injected untrusted script from exporting private trusted module functions. + if (Context.EngineSessionState.Module != null && + Context.LanguageMode != Context.EngineSessionState.Module.LanguageMode) + { + var se = new PSSecurityException(Modules.CannotExportMembersAccrossLanguageBoundaries); + var er = new ErrorRecord(se, "Modules_CannotExportMembersAccrossLanguageBoundaries", ErrorCategory.SecurityError, this); + ThrowTerminatingError(er); + } + ModuleIntrinsics.ExportModuleMembers(this, this.Context.EngineSessionState, _functionPatterns, _cmdletPatterns, _aliasPatterns, _variablePatterns, null); diff --git a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs index 50d09e195db..c65bf5c53e5 100644 --- a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs @@ -387,7 +387,7 @@ private void ImportModule_ViaLocalModuleInfo(ImportModuleOptions importModuleOpt try { PSModuleInfo alreadyLoadedModule = null; - Context.Modules.ModuleTable.TryGetValue(module.Path, out alreadyLoadedModule); + TryGetFromModuleTable(module.Path, out alreadyLoadedModule); if (!BaseForce && DoesAlreadyLoadedModuleSatisfyConstraints(alreadyLoadedModule)) { AddModuleToModuleTables(this.Context, this.TargetSessionState.Internal, alreadyLoadedModule); @@ -418,7 +418,7 @@ private void ImportModule_ViaLocalModuleInfo(ImportModuleOptions importModuleOpt else { PSModuleInfo moduleToRemove; - if (Context.Modules.ModuleTable.TryGetValue(module.Path, out moduleToRemove)) + if (TryGetFromModuleTable(module.Path, out moduleToRemove, toRemove: true)) { Dbg.Assert(BaseForce, "We should only remove and reload if -Force was specified"); RemoveModule(moduleToRemove); @@ -579,12 +579,11 @@ private PSModuleInfo ImportModule_LocallyViaName(ImportModuleOptions importModul // TODO/FIXME: use IsModuleAlreadyLoaded to get consistent behavior // TODO/FIXME: (for example checking ModuleType != Manifest below seems incorrect - cdxml modules also declare their own version) // PSModuleInfo alreadyLoadedModule = null; - // Context.Modules.ModuleTable.TryGetValue(rootedPath, out alreadyLoadedModule); + // TryGetFromModuleTable(rootedPath, out alreadyLoadedModule); // if (!BaseForce && IsModuleAlreadyLoaded(alreadyLoadedModule)) // If the module has already been loaded, just emit it and continue... - PSModuleInfo module; - if (!BaseForce && Context.Modules.ModuleTable.TryGetValue(rootedPath, out module)) + if (!BaseForce && TryGetFromModuleTable(rootedPath, out PSModuleInfo module)) { if (module.ModuleType != ModuleType.Manifest || ModuleIntrinsics.IsVersionMatchingConstraints(module.Version, RequiredVersion, BaseMinimumVersion, BaseMaximumVersion)) @@ -623,7 +622,7 @@ private PSModuleInfo ImportModule_LocallyViaName(ImportModuleOptions importModul if (File.Exists(rootedPath)) { PSModuleInfo moduleToRemove; - if (Context.Modules.ModuleTable.TryGetValue(rootedPath, out moduleToRemove)) + if (TryGetFromModuleTable(rootedPath, out moduleToRemove, toRemove: true)) { RemoveModule(moduleToRemove); } @@ -1018,9 +1017,8 @@ private PSModuleInfo ImportModule_RemotelyViaPsrpSession_SinglePreimportedModule // // make sure the temporary folder gets removed when the module is removed // - PSModuleInfo moduleInfo; string psm1Path = Path.Combine(temporaryModulePath, Path.GetFileName(temporaryModulePath) + ".psm1"); - if (!this.Context.Modules.ModuleTable.TryGetValue(psm1Path, out moduleInfo)) + if (!TryGetFromModuleTable(psm1Path, out PSModuleInfo moduleInfo, toRemove: true)) { if (Directory.Exists(temporaryModulePath)) { diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 79065672b80..95e791a1b5f 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -18,6 +18,8 @@ using System.Text; using System.Xml; using Microsoft.PowerShell.Cmdletization; +using System.Management.Automation.Language; + using Dbg = System.Management.Automation.Diagnostics; // @@ -88,6 +90,12 @@ protected internal struct ImportModuleOptions /// If Scope parameter is Local, this is true. /// internal bool Local; + + /// + /// Lets nested module import to export all of its functions, regardless of language boundaries. + /// This will be allowed when the manifest explicitly exports functions which will limit all visible module functions. + /// + internal bool AllowNestedModuleFunctionsToExport; } /// @@ -603,7 +611,7 @@ private bool ValidateManifestHash( private PSModuleInfo LoadModuleNamedInManifest(PSModuleInfo parentModule, ModuleSpecification moduleSpecification, string moduleBase, bool searchModulePath, string prefix, SessionState ss, ImportModuleOptions options, ManifestProcessingFlags manifestProcessingFlags, bool loadTypesFiles, - bool loadFormatFiles, object privateData, out bool found, string shortModuleName) + bool loadFormatFiles, object privateData, out bool found, string shortModuleName, PSLanguageMode? manifestLanguageMode) { PSModuleInfo module = null; PSModuleInfo tempModuleInfoFromVerification = null; @@ -786,6 +794,21 @@ private PSModuleInfo LoadModuleNamedInManifest(PSModuleInfo parentModule, Module } } + if (manifestLanguageMode.HasValue && found && (module != null) && module.LanguageMode.HasValue) + { + // Check for script module language mode consistency. All loaded script modules must have the same language mode as the manifest. + // If not then this indicates a malformed module and a possible exploit to make trusted private functions visible in a + // Constrained Language session. + if (module.LanguageMode != manifestLanguageMode) + { + var languageModeError = PSTraceSource.NewInvalidOperationException( + Modules.MismatchedLanguageModes, + module.Name, manifestLanguageMode, module.LanguageMode); + languageModeError.SetErrorId("Modules_MismatchedLanguageModes"); + throw languageModeError; + } + } + // At this point, we haven't found an actual module, so try loading it as a // PSSnapIn and then finally as an assembly in the GAC... if ((found == false) && (moduleSpecification.Guid == null) && (moduleSpecification.Version == null) && (moduleSpecification.RequiredVersion == null) && (moduleSpecification.MaximumVersion == null)) @@ -1393,7 +1416,7 @@ private ErrorRecord GetErrorRecordIfUnsupportedRootCdxmlAndNestedModuleScenario( /// Routine to process the module manifest data language script. /// /// The path to the manifest file - /// The script info for the manifest script + /// The script info for the manifest script /// Contents of the module manifest /// Contents of the localized module manifest /// processing flags (whether to write errors / load elements) @@ -1406,7 +1429,7 @@ private ErrorRecord GetErrorRecordIfUnsupportedRootCdxmlAndNestedModuleScenario( /// internal PSModuleInfo LoadModuleManifest( string moduleManifestPath, - ExternalScriptInfo scriptInfo, + ExternalScriptInfo manifestScriptInfo, Hashtable data, Hashtable localizedData, ManifestProcessingFlags manifestProcessingFlags, @@ -1570,14 +1593,14 @@ internal PSModuleInfo LoadModuleManifest( string mtpExtension = Path.GetExtension(rootedPath); if (!string.IsNullOrEmpty(mtpExtension) && ModuleIntrinsics.IsPowerShellModuleExtension(mtpExtension)) { - Context.Modules.ModuleTable.TryGetValue(rootedPath, out loadedModule); + TryGetFromModuleTable(rootedPath, out loadedModule); } else { foreach (string extensionToTry in ModuleIntrinsics.PSModuleExtensions) { rootedPath = this.FixupFileName(moduleBase, actualRootModule, extensionToTry); - Context.Modules.ModuleTable.TryGetValue(rootedPath, out loadedModule); + TryGetFromModuleTable(rootedPath, out loadedModule); if (loadedModule != null) break; } @@ -2889,6 +2912,7 @@ internal PSModuleInfo LoadModuleManifest( BaseDisableNameChecking = true; SessionStateInternal oldSessionState = Context.EngineSessionState; + var exportedFunctionsContainsWildcards = ModuleIntrinsics.PatternContainsWildcard(exportedFunctions); try { if (importingModule) @@ -2899,6 +2923,7 @@ internal PSModuleInfo LoadModuleManifest( if (ss != null) { + ss.Internal.ManifestWithExplicitFunctionExport = !exportedFunctionsContainsWildcards; Context.EngineSessionState = ss.Internal; } @@ -2907,6 +2932,11 @@ internal PSModuleInfo LoadModuleManifest( // For nested modules, we need to set importmoduleoptions to false as they should not use the options set for parent module ImportModuleOptions nestedModuleOptions = new ImportModuleOptions(); + // If the nested manifest explicitly (no wildcards) specifies functions to be exported then allow all functions to be exported + // into the session state function table (regardless of language boundaries), because the manifest will filter them later to the + // specified function list. + nestedModuleOptions.AllowNestedModuleFunctionsToExport = ((exportedFunctions != null) && !exportedFunctionsContainsWildcards); + foreach (ModuleSpecification nestedModuleSpecification in nestedModules) { bool found = false; @@ -2915,19 +2945,20 @@ internal PSModuleInfo LoadModuleManifest( this.BaseGlobal = false; PSModuleInfo nestedModule = LoadModuleNamedInManifest( - manifestInfo, - nestedModuleSpecification, // moduleName - moduleBase, - true, // searchModulePath - string.Empty, // prefix: no -Prefix added for nested modules - null, - nestedModuleOptions, - manifestProcessingFlags, - true, - true, - privateData, - out found, - shortModuleName: null); + parentModule: manifestInfo, + moduleSpecification: nestedModuleSpecification, + moduleBase: moduleBase, + searchModulePath: true, + prefix: string.Empty, + ss: null, + options: nestedModuleOptions, + manifestProcessingFlags: manifestProcessingFlags, + loadTypesFiles: true, + loadFormatFiles: true, + privateData: privateData, + found: out found, + shortModuleName: null, + manifestLanguageMode: ((manifestScriptInfo != null) ? manifestScriptInfo.DefiningLanguageMode.GetValueOrDefault() : (PSLanguageMode?)null)); this.BaseGlobal = oldGlobal; @@ -3016,16 +3047,21 @@ internal PSModuleInfo LoadModuleManifest( try { bool found; - newManifestInfo = LoadModuleNamedInManifest(null, new ModuleSpecification(actualRootModule), - moduleBase, /* searchModulePath */ false, - resolvedCommandPrefix, ss, options, manifestProcessingFlags, - // If types files already loaded, don't load snapin files - (exportedTypeFiles == null || 0 == exportedTypeFiles.Count), - // if format files already loaded, don't load snapin files - (exportedFormatFiles == null || 0 == exportedFormatFiles.Count), - privateData, - out found, - null); + newManifestInfo = LoadModuleNamedInManifest( + parentModule: null, + moduleSpecification: new ModuleSpecification(actualRootModule), + moduleBase: moduleBase, + searchModulePath: false, + prefix: resolvedCommandPrefix, + ss: ss, + options: options, + manifestProcessingFlags: manifestProcessingFlags, + loadTypesFiles: (exportedTypeFiles == null || 0 == exportedTypeFiles.Count), // If types files already loaded, don't load snapin files + loadFormatFiles: (exportedFormatFiles == null || 0 == exportedFormatFiles.Count), // if format files already loaded, don't load snapin files + privateData: privateData, + found: out found, + shortModuleName: null, + manifestLanguageMode: ((manifestScriptInfo != null) ? manifestScriptInfo.DefiningLanguageMode.GetValueOrDefault() : (PSLanguageMode?)null)); if (!found || (newManifestInfo == null)) { @@ -3348,9 +3384,17 @@ internal PSModuleInfo LoadModuleManifest( // implicitly export functions and cmdlets. if ((ss != null) && (!ss.Internal.UseExportList)) { - ModuleIntrinsics.ExportModuleMembers(this, + // For cross language boundaries, implicitly import all functions only if + // this manifest *does* exort functions explicitly. + List fnMatchPattern = ( + (manifestScriptInfo.DefiningLanguageMode == PSLanguageMode.FullLanguage) && + (Context.LanguageMode != PSLanguageMode.FullLanguage) && + (exportedFunctions == null) + ) ? null : MatchAll; + + ModuleIntrinsics.ExportModuleMembers(cmdlet: this, sessionState: ss.Internal, - functionPatterns: MatchAll, + functionPatterns: fnMatchPattern, cmdletPatterns: MatchAll, aliasPatterns: null, variablePatterns: null, @@ -3362,6 +3406,22 @@ internal PSModuleInfo LoadModuleManifest( { if (ss != null) { + // If module (psm1) functions were not exported because of cross language boundary restrictions, + // then implicitly export them here so that they can be filtered by the exportedFunctions list. + // Unless exportedFunctions contains the wildcard character that isn't allowed across language + // boundaries. + if (!ss.Internal.FunctionsExported && !exportedFunctionsContainsWildcards) + { + ModuleIntrinsics.ExportModuleMembers( + cmdlet: this, + sessionState: ss.Internal, + functionPatterns: MatchAll, + cmdletPatterns: null, + aliasPatterns: null, + variablePatterns: null, + doNotExportCmdlets: null); + } + Dbg.Assert(ss.Internal.ExportedFunctions != null, "ss.Internal.ExportedFunctions should not be null"); @@ -3458,6 +3518,8 @@ internal PSModuleInfo LoadModuleManifest( ImportModuleMembers(manifestInfo, resolvedCommandPrefix, options); } + manifestInfo.LanguageMode = (manifestScriptInfo != null) ? manifestScriptInfo.DefiningLanguageMode : (PSLanguageMode?)null; + return manifestInfo; } @@ -5031,8 +5093,7 @@ internal bool DoesAlreadyLoadedModuleSatisfyConstraints(PSModuleInfo alreadyLoad /// internal PSModuleInfo IsModuleImportUnnecessaryBecauseModuleIsAlreadyLoaded(string modulePath, string prefix, ImportModuleOptions options) { - PSModuleInfo alreadyLoadedModule; - if (this.Context.Modules.ModuleTable.TryGetValue(modulePath, out alreadyLoadedModule)) + if (TryGetFromModuleTable(modulePath, out PSModuleInfo alreadyLoadedModule)) { if (this.DoesAlreadyLoadedModuleSatisfyConstraints(alreadyLoadedModule)) { @@ -5140,7 +5201,6 @@ internal PSModuleInfo LoadUsingExtensions(PSModuleInfo parentModule, string prefix, SessionState ss, ImportModuleOptions options, ManifestProcessingFlags manifestProcessingFlags, out bool found, out bool moduleFileFound) { string[] extensions; - PSModuleInfo module; moduleFileFound = false; if (!string.IsNullOrEmpty(extension)) @@ -5171,11 +5231,11 @@ internal PSModuleInfo LoadUsingExtensions(PSModuleInfo parentModule, } // If the module has already been loaded, just emit it and continue... - Context.Modules.ModuleTable.TryGetValue(fileName, out module); + TryGetFromModuleTable(fileName, out PSModuleInfo module); if (!BaseForce && importingModule && DoesAlreadyLoadedModuleSatisfyConstraints(module)) { moduleFileFound = true; - module = Context.Modules.ModuleTable[fileName]; + // If the module has already been loaded, then while loading it the second time, we should load it with the DefaultCommandPrefix specified in the module manifest. (If there is no Prefix from command line) if (string.IsNullOrEmpty(prefix)) { @@ -5401,7 +5461,7 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str // If the module is in memory and the versions don't match don't return it. // This will allow the search to continue and load a different version of the module. - if (Context.Modules.ModuleTable.TryGetValue(fileName, out module)) + if (TryGetFromModuleTable(fileName, out module)) { if (!ModuleIntrinsics.IsVersionMatchingConstraints(module.Version, minimumVersion: BaseMinimumVersion, maximumVersion: BaseMaximumVersion)) { @@ -5414,7 +5474,18 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str found = false; string scriptName; - ExternalScriptInfo scriptInfo = null; + + // + // !!NOTE!! + // If a new module type to load is ever added and if that new module type is based on a script file, + // such as the existing .psd1 and .psm1 files, + // then be sure to include the script file LanguageMode in the moduleInfo type created for the loaded module. + // The PSModuleInfo.LanguageMode property is used to check consistency between the manifest (.psd1) file + // and all other script (.psm1) file based modules being loaded by that manifest. + // Use the PSModuleInfo class constructor that takes the PSLanguageMode parameter argument. + // Look at the LoadModuleNamedInManifest() method to see how the language mode check works. + // !!NOTE!! + // string _origModuleBeingProcessed = Context.ModuleBeingProcessed; try @@ -5443,15 +5514,15 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str } else { - scriptInfo = GetScriptInfoForFile(fileName, out scriptName, true); + var psm1ScriptInfo = GetScriptInfoForFile(fileName, out scriptName, true); try { - Context.Modules.IncrementModuleNestingDepth(this, scriptInfo.Path); + Context.Modules.IncrementModuleNestingDepth(this, psm1ScriptInfo.Path); // Create the module object... try { - module = Context.Modules.CreateModule(fileName, scriptInfo, MyInvocation.ScriptPosition, ss, privateData, BaseArgumentList); + module = Context.Modules.CreateModule(fileName, psm1ScriptInfo, MyInvocation.ScriptPosition, ss, privateData, BaseArgumentList); module.SetModuleBase(moduleBase); SetModuleLoggingInformation(module); @@ -5460,9 +5531,39 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str // implicitly export functions and cmdlets. if (!module.SessionState.Internal.UseExportList) { - ModuleIntrinsics.ExportModuleMembers(this, module.SessionState.Internal, functionPatterns: MatchAll, - cmdletPatterns: MatchAll, aliasPatterns: MatchAll, variablePatterns: null, doNotExportCmdlets: null); + // For cross language boundaries don't implicitly export all functions, unless they are allowed nested modules. + // Implict function export is allowed when any of the following is true: + // - Nested modules are allowed by module manifest + // - The import context language mode is FullLanguage + // - This script module not running as trusted (FullLanguage) + module.ModuleAutoExportsAllFunctions = options.AllowNestedModuleFunctionsToExport || + Context.LanguageMode == PSLanguageMode.FullLanguage || + psm1ScriptInfo.DefiningLanguageMode != PSLanguageMode.FullLanguage; + + List fnMatchPattern = module.ModuleAutoExportsAllFunctions ? MatchAll : null; + + ModuleIntrinsics.ExportModuleMembers( + cmdlet: this, + sessionState: module.SessionState.Internal, + functionPatterns: fnMatchPattern, + cmdletPatterns: MatchAll, + aliasPatterns: MatchAll, + variablePatterns: null, + doNotExportCmdlets: null); } + else if ((SystemPolicy.GetSystemLockdownPolicy() == SystemEnforcementMode.Enforce) && + (module.LanguageMode == PSLanguageMode.FullLanguage) && + module.SessionState.Internal.FunctionsExportedWithWildcard && + !module.SessionState.Internal.ManifestWithExplicitFunctionExport) + { + // When in a constrained environment and functions are being exported from this module using wildcards, make sure + // exported functions only come from this module and not from any imported nested modules. + // Unless there is a parent manifest that explicitly filters all exported functions (no wildcards). + // This prevents unintended public exposure of imported functions running in FullLanguage. + ModuleIntrinsics.RemoveNestedModuleFunctions(module); + } + + CheckForDisallowedDotSourcing(module.SessionState, psm1ScriptInfo, options); // Add it to the all module tables ImportModuleMembers(module, prefix, options); @@ -5523,16 +5624,15 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str // Removing the module will not remove the commands dot-sourced from the .ps1 file. // This module info is created so that we can keep the behavior consistent between scripts imported as modules and other kind of modules(all of them should have a PSModuleInfo). // Auto-loading expects we always have a PSModuleInfo object for any module. This is how this issue was found. - module = new PSModuleInfo(ModuleIntrinsics.GetModuleName(fileName), fileName, Context, ss); - - scriptInfo = GetScriptInfoForFile(fileName, out scriptName, true); + var ps1ScriptInfo = GetScriptInfoForFile(fileName, out scriptName, true); + Dbg.Assert(ps1ScriptInfo != null, "Scriptinfo for dotted file can't be null"); + module = new PSModuleInfo(ModuleIntrinsics.GetModuleName(fileName), fileName, Context, ss, ps1ScriptInfo.DefiningLanguageMode); message = StringUtil.Format(Modules.DottingScriptFile, fileName); WriteVerbose(message); try { - Dbg.Assert(scriptInfo != null, "Scriptinfo for dotted file can't be null"); found = true; InvocationInfo oldInvocationInfo = (InvocationInfo)Context.GetVariableValue(SpecialVariables.MyInvocationVarPath); @@ -5541,9 +5641,8 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str try { - InvocationInfo invocationInfo = new InvocationInfo(scriptInfo, scriptInfo.ScriptBlock.Ast.Extent, - Context); - scriptInfo.ScriptBlock.InvokeWithPipe( + InvocationInfo invocationInfo = new InvocationInfo(ps1ScriptInfo, ps1ScriptInfo.ScriptBlock.Ast.Extent, Context); + ps1ScriptInfo.ScriptBlock.InvokeWithPipe( useLocalScope: false, errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe, dollarUnder: AutomationNull.Value, @@ -5603,11 +5702,11 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str } else if (ext.Equals(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase)) { - scriptInfo = GetScriptInfoForFile(fileName, out scriptName, true); + var psd1ScriptInfo = GetScriptInfoForFile(fileName, out scriptName, true); found = true; - Dbg.Assert(scriptInfo != null, "Scriptinfo for module manifest (.psd1) can't be null"); + Dbg.Assert(psd1ScriptInfo != null, "Scriptinfo for module manifest (.psd1) can't be null"); module = LoadModuleManifest( - scriptInfo, + psd1ScriptInfo, manifestProcessingFlags, BaseMinimumVersion, BaseMaximumVersion, @@ -5617,6 +5716,8 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str if (module != null) { + CheckForDisallowedDotSourcing(module.SessionState, psd1ScriptInfo, options); + if (importingModule) { // Add it to all the module tables @@ -5639,6 +5740,9 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str moduleBase, ss, options, manifestProcessingFlags, prefix, true, true, out found); if (found && module != null) { + // LanguageMode does not apply to binary modules + module.LanguageMode = (PSLanguageMode?)null; + if (importingModule) { // Add it to all the module tables @@ -5667,12 +5771,12 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str try { string moduleName = ModuleIntrinsics.GetModuleName(fileName); - scriptInfo = GetScriptInfoForFile(fileName, out scriptName, true); + var cdxmlScriptInfo = GetScriptInfoForFile(fileName, out scriptName, true); try { // generate cmdletization proxies - var cmdletizationXmlReader = new StringReader(scriptInfo.ScriptContents); + var cmdletizationXmlReader = new StringReader(cdxmlScriptInfo.ScriptContents); var cmdletizationProxyModuleWriter = new StringWriter(CultureInfo.InvariantCulture); var scriptWriter = new ScriptWriter( cmdletizationXmlReader, @@ -5683,7 +5787,7 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str if (!importingModule) { - module = new PSModuleInfo(fileName, null, null); + module = new PSModuleInfo(null, fileName, null, null, cdxmlScriptInfo.DefiningLanguageMode); scriptWriter.PopulatePSModuleInfo(module); scriptWriter.ReportExportedCommands(module, prefix); } @@ -5773,6 +5877,45 @@ internal PSModuleInfo LoadModule(PSModuleInfo parentModule, string fileName, str return module; } + private void CheckForDisallowedDotSourcing( + SessionState ss, + ExternalScriptInfo scriptInfo, + ImportModuleOptions options) + { + if (ss == null || ss.Internal == null) + { return; } + + // A manifest with explicit function export is detected through a shared session state or the nested module options, because nested + // module processing does not use a shared session state. + var manifestWithExplicitFunctionExport = ss.Internal.ManifestWithExplicitFunctionExport || options.AllowNestedModuleFunctionsToExport; + + // If system is in lock down mode, we disallow trusted modules that use the dotsource operator while simultaneously using + // wild cards for exporting module functions, unless there is an overriding manifest that explicitly exports functions + // without wild cards. + // This is because dotsourcing brings functions into module scope and it is too easy to inadvertently or maliciously + // expose harmful private functions that run in trusted (FullLanguage) mode. + if (!manifestWithExplicitFunctionExport && ss.Internal.FunctionsExportedWithWildcard && + (SystemPolicy.GetSystemLockdownPolicy() == SystemEnforcementMode.Enforce) && + (scriptInfo.DefiningLanguageMode == PSLanguageMode.FullLanguage)) + { + var dotSourceOperator = scriptInfo.GetScriptBlockAst().FindAll(ast => + { + var cmdAst = ast as CommandAst; + return (cmdAst?.InvocationOperator == TokenKind.Dot); + }, + searchNestedScriptBlocks: true).FirstOrDefault(); + + if (dotSourceOperator != null) + { + var errorRecord = new ErrorRecord( + new PSSecurityException(Modules.CannotUseDotSourceWithWildCardFunctionExport), + "Modules_SystemLockDown_CannotUseDotSourceWithWildCardFunctionExport", + ErrorCategory.SecurityError, null); + ThrowTerminatingError(errorRecord); + } + } + } + private static bool ShouldProcessScriptModule(PSModuleInfo parentModule, ref bool found) { bool shouldProcessModule = true; @@ -7279,6 +7422,54 @@ internal static PSSnapInInfo GetEngineSnapIn(ExecutionContext context, string na return null; } + + /// + /// Returns the context cached ModuleTable module for import only if found and has safe language boundaries while + /// exporting all functions by default. + /// + /// This protects cached trusted modules that exported all functions in a trusted context, from being re-used + /// in an untrusted context and thus exposing functions that were meant to be private in that context. + /// + /// Returning false forces module import to re-import the module from file with the current context and prevent + /// all module functions from being exported by default. + /// + /// Note that module loading order is important with this check when the system is *locked down with DeviceGuard*. + /// If a submodule that does not explicitly export any functions is imported from the command line, its useless + /// because no functions are exported (default fn export is explictly disallowed on locked down systems). + /// But if a parentmodule that imports the submodule is then imported, it will get the useless version of the + /// module from the ModuleTable and the parent module will not work. + /// $mSub = import-module SubModule # No functions exported, useless + /// $mParent = import-module ParentModule # This internally imports SubModule + /// $mParent.DoSomething # This will likely be broken because SubModule functions are not accessible + /// But this is not a realistic scenario because SubModule is useless with DeviceGuard lock down and must explicitly + /// export its functions to become useful, at which point this check is no longer in effect and there is no issue. + /// $mSub = import-module SubModule # Explictly exports functions, useful + /// $mParent = import-module ParentModule # This internally imports SubModule + /// $mParent.DoSomething # This works because SubModule functions are exported and accessible + /// + /// Key + /// PSModuleInfo + /// True if module item is to be removed + /// True if module found in table and is safe to use + internal bool TryGetFromModuleTable(string key, out PSModuleInfo moduleInfo, bool toRemove = false) + { + var foundModule = Context.Modules.ModuleTable.TryGetValue(key, out moduleInfo); + + // Check for unsafe language modes between module load context and current context. + // But only for script modules that exported all functions in a trusted (FL) context. + if (foundModule && + !toRemove && + moduleInfo.ModuleType == ModuleType.Script && + Context.LanguageMode == PSLanguageMode.ConstrainedLanguage && + moduleInfo.LanguageMode == PSLanguageMode.FullLanguage && + moduleInfo.ModuleAutoExportsAllFunctions) + { + moduleInfo = null; + return false; + } + + return foundModule; + } } // end ModuleCmdletBase /// diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index 732abffb638..ee7a0f89cd0 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -194,6 +194,7 @@ private PSModuleInfo CreateModuleImplementation(string name, string path, object throw PSTraceSource.NewInvalidOperationException(); sb.SessionStateInternal = ss.Internal; + module.LanguageMode = sb.LanguageMode; InvocationInfo invocationInfo = new InvocationInfo(scriptInfo, scriptPosition); @@ -1327,6 +1328,29 @@ private static string ProcessOneModulePath(ExecutionContext context, string envP return null; } + /// + /// Removes all functions not belonging to the parent module. + /// + /// Parent module + static internal void RemoveNestedModuleFunctions(PSModuleInfo module) + { + var input = module.SessionState?.Internal?.ExportedFunctions; + if ((input == null) || (input.Count == 0)) + { return; } + + List output = new List(input.Count); + foreach (var fnInfo in input) + { + if (module.Name.Equals(fnInfo.ModuleName, StringComparison.OrdinalIgnoreCase)) + { + output.Add(fnInfo); + } + } + + input.Clear(); + input.AddRange(output); + } + private static void SortAndRemoveDuplicates(List input, Func keyGetter) { Dbg.Assert(input != null, "Caller should verify that input != null"); @@ -1385,6 +1409,12 @@ internal static void ExportModuleMembers( if (functionPatterns != null) { + sessionState.FunctionsExported = true; + if (PatternContainsWildcard(functionPatterns)) + { + sessionState.FunctionsExportedWithWildcard = true; + } + IDictionary ft = sessionState.ModuleScope.FunctionTable; foreach (KeyValuePair entry in ft) @@ -1523,6 +1553,27 @@ internal static void ExportModuleMembers( } } + /// + /// Checks pattern list for wildcard characters. + /// + /// Pattern list + /// True if pattern contains '*' + internal static bool PatternContainsWildcard(List list) + { + if (list != null) + { + foreach (var item in list) + { + if (WildcardPattern.ContainsWildcardCharacters(item.Pattern)) + { + return true; + } + } + } + + return false; + } + private static AliasInfo NewAliasInfo(AliasInfo alias, SessionStateInternal sessionState) { Dbg.Assert(alias != null, "alias should not be null"); diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index f4142837d5c..7cb64c0beb3 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -51,6 +51,20 @@ internal PSModuleInfo(string path, ExecutionContext context, SessionState sessio { } + /// + /// This object describes a PowerShell module... + /// + /// The name to use for the module. If null, get it from the path name + /// The absolute path to the module + /// The execution context for this engine instance + /// The module's sessionstate object - this may be null if the module is a dll. + /// Language mode for script based modules + internal PSModuleInfo(string name, string path, ExecutionContext context, SessionState sessionState, PSLanguageMode? languageMode) + : this(name, path, context, sessionState) + { + LanguageMode = languageMode; + } + /// /// This object describes a PowerShell module... /// @@ -134,6 +148,8 @@ public PSModuleInfo(ScriptBlock scriptBlock) SessionState = new SessionState(context, true, true); SessionState.Internal.Module = this; + LanguageMode = scriptBlock.LanguageMode; + // Now set up the module's session state to be the current session state SessionStateInternal oldSessionState = context.EngineSessionState; try @@ -164,6 +180,20 @@ public PSModuleInfo(ScriptBlock scriptBlock) } } + /// + /// Specifies the language mode for script based modules + /// + internal PSLanguageMode? LanguageMode + { + get; + set; + } = PSLanguageMode.FullLanguage; + + /// + /// Set to true when script module automatically exports all functions by default + /// + internal bool ModuleAutoExportsAllFunctions { get; set; } + internal bool ModuleHasPrivateMembers { get; set; } /// diff --git a/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs b/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs index 7375aad41c6..71147018ea1 100644 --- a/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs +++ b/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs @@ -105,6 +105,33 @@ internal IDictionary GetFunctionTableAtScope(string scopeI internal bool UseExportList { get; set; } = false; + /// + /// Set to true when module functions are being explicitly exported using Export-ModuleMember + /// + internal bool FunctionsExported { get; set; } + + /// + /// Set to true when any processed module functions are being explicitly exported using '*' wildcard + /// + internal bool FunctionsExportedWithWildcard + { + get { return _functionsExportedWithWildcard; } + set + { + Dbg.Assert((value == true), "This property should never be set/reset to false"); + if (value == true) + { + _functionsExportedWithWildcard = value; + } + } + } + private bool _functionsExportedWithWildcard; + + /// + /// Set to true if module loading is performed under a manifest that explicitly exports functions (no wildcards) + /// + internal bool ManifestWithExplicitFunctionExport { get; set; } + /// /// Get a functions out of session state. /// @@ -720,4 +747,4 @@ internal void RemoveFunction(string name, PSModuleInfo module) #endregion Functions } // SessionStateInternal class -} \ No newline at end of file +} diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs index 8f60a0baa68..1839eab9c12 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs @@ -51,6 +51,14 @@ private static CommandProcessorBase AddCommand(PipelineProcessor pipe, throw InterpreterError.NewInterpreterException(null, typeof(RuntimeException), null, "CantInvokeInNonImportedModule", ParserStrings.CantInvokeInNonImportedModule, mi.Name); } + else if (((invocationToken == TokenKind.Ampersand) || (invocationToken == TokenKind.Dot)) && (mi.LanguageMode != context.LanguageMode)) + { + // Disallow FullLanguage "& (Get-Module MyModule) MyPrivateFn" from ConstrainedLanguage because it always + // runs "internal" origin and so has access to all functions, including non-exported functions. + // Otherwise we end up leaking non-exported functions that run in FullLanguage. + throw InterpreterError.NewInterpreterException(null, typeof(RuntimeException), null, + "CantInvokeCallOperatorAcrossLanguageBoundaries", ParserStrings.CantInvokeCallOperatorAcrossLanguageBoundaries); + } commandSessionState = mi.SessionState.Internal; commandIndex += 1; } diff --git a/src/System.Management.Automation/resources/Modules.resx b/src/System.Management.Automation/resources/Modules.resx index 97597cc827d..6c1f5b1a9a0 100644 --- a/src/System.Management.Automation/resources/Modules.resx +++ b/src/System.Management.Automation/resources/Modules.resx @@ -621,4 +621,13 @@ Importing *.ps1 files as modules is not allowed in ConstrainedLanguage mode. + + An error has occurred while loading script module {0} because it has a different language mode than the module manifest. The manifest language mode is {1} and the module language mode is {2}. Ensure all module files are signed or otherwise part of your application allow list configuration. + + + This module uses the dot-source operator while exporting functions using wildcard characters, and this is disallowed when the system is under application verification enforcement. + + + Cannot export module members from a module that has a different language mode from the running session. + diff --git a/src/System.Management.Automation/resources/ParserStrings.resx b/src/System.Management.Automation/resources/ParserStrings.resx index b0f50665bfa..6cd368daffd 100644 --- a/src/System.Management.Automation/resources/ParserStrings.resx +++ b/src/System.Management.Automation/resources/ParserStrings.resx @@ -1491,4 +1491,7 @@ ModuleVersion : Version of module to import. If used, ModuleName must represent This script contains content that has been flagged as suspicious through a policy setting and has been blocked with error code {0}. Contact your administrator for more information. + + Cannot use '&' or '.' operators to invoke a module scope command across language boundaries. + diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageModules.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageModules.Tests.ps1 new file mode 100644 index 00000000000..f96f4b88768 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageModules.Tests.ps1 @@ -0,0 +1,1411 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# 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 + +$defaultParamValues = $PSDefaultParameterValues.Clone() +$PSDefaultParameterValues["it:Skip"] = !$IsWindows + +try +{ + Describe "Export-ModuleMember should not work across language boundaries" -Tags 'Feature','RequireAdminOnWindows' { + + BeforeAll { + + $script = @' + function IEXInjectableFunction + { + param ([string] $path) + Invoke-Expression -Command "dir $path" + } + + function PrivateAddTypeAndRun + { + param ([string] $source) + $type = Add-Type -TypeDefinition $source -passthru + $type::new() + } + + Export-ModuleMember -Function IEXInjectableFunction +'@ + + $modulePathName = "modulePath_$(Get-Random -Max 9999)" + $modulePath = Join-Path $testdrive $modulePathName + mkdir $modulePath + $trustedModuleFile = Join-Path $modulePath "T1TestModule_System32.psm1" + $script | Out-File -FilePath $trustedModuleFile + } + + AfterAll { + + Remove-Module -Name T1TestModule_System32 -Force -ErrorAction Ignore + + } + + It "Verifies that IEX running in ConstrainedLanguage cannot export functions from trusted module" { + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + + Import-Module -Name $trustedModuleFile -Force + + # Use the vulnerable IEXInjectableFunction function to export all functions from module + # Note that Invoke-Expression will run in constrained language mode because it is known to be vulnerable + T1TestModule_System32\IEXInjectableFunction -path 'c:\windows\system32\CodeIntegrity; Export-ModuleMember -Function *' + throw "No Error!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode -RevertLockdownMode + } + + # A security error should be thrown + $expectedError.FullyQualifiedErrorId | Should -BeExactly "Modules_CannotExportMembersAccrossLanguageBoundaries,Microsoft.PowerShell.Commands.ExportModuleMemberCommand" + + # PrivateAddTypeAndRun private function should not be exposed + $result = Get-Command -Name T1TestModule_System32\PrivateAddTypeAndRun 2>$null + $result | Should -BeNullOrEmpty + } + } + + Describe "Dot-source operator is not allowed in modules on locked down systems that export functions with wildcards" -Tags 'Feature','RequireAdminOnWindows' { + + BeforeAll { + + $TestModulePath = Join-Path $TestDrive "Modules_$(Get-Random -Maximum 99999)" + New-Item -Path $TestModulePath -ItemType Directory -Force -ErrorAction SilentlyContinue + + # Module that dot sources ps1 file while and exports functions with wildcard. + $scriptModuleNameA = "ModuleDotSourceWildcard_System32" + $moduleFilePathA = Join-Path $TestModulePath ($scriptModuleNameA + ".psm1") + $dotSourceNameA = "DotSourceFileNoWildCard_System32" + $dotSourceFilePathA = Join-Path $TestModulePath ($dotSourceNameA + ".ps1") + @' + function PublicDSFnA { "PublicDSFnA"; PrivateDSFnA } + function PrivateDSFnA { "PrivateDSFnA" } +'@ | Out-File -FilePath $dotSourceFilePathA + @' + . {0} + function PublicFnA {{ "PublicFnA"; PublicDSFnA }} + function PrivateFnA {{ "PrivateFnA"; PrivateDSFnA }} + + Export-ModuleMember -Function "*" +'@ -f $dotSourceFilePathA | Out-File -FilePath $moduleFilePathA + + # Module that dot sources ps1 file that exports module functions. Parent module exports nothing. + $scriptModuleNameB = "ModuleDotSourceNoExport_System32" + $moduleFilePathB = Join-Path $TestModulePath ($scriptModuleNameB + ".psm1") + $dotSourceNameB = "DotSourceFileWildCard_System32" + $dotSourceFilePathB = Join-Path $TestModulePath ($dotSourceNameB + ".ps1") + @' + function PublicDSFnB { "PublicDSFnB"; PrivateDSFnB } + function PrivateDSFnB { "PrivateDSFnB" } + + Export-ModuleMember -Function "*" +'@ | Out-File -FilePath $dotSourceFilePathB + @' + . {0} + function PublicFnB {{ "PublicFnB"; PrivateFnB }} + function PrivateFnB {{ "PrivateFnB" }} +'@ -f $dotSourceFilePathB | Out-File -FilePath $moduleFilePathB + + # Module that dot sources ps1 file and exports functions with wildcard, but has overriding manifest. + $scriptModuleNameC = "ModuleDotSourceWildCardM_System32" + $moduleFilePathC = Join-Path $TestModulePath ($scriptModuleNameC + ".psm1") + $dotSourceNameC = "DotSourceFileNoWildCardM_System32" + $dotSourceFilePathC = Join-Path $TestModulePath ($dotSourceNameC + ".ps1") + $manifestFilePathC = Join-Path $TestModulePath ($scriptModuleNameC + ".psd1") + @' + function PublicDSFnC { "PublicDSFnC"; PrivateDSFnC } + function PrivateDSFnC { "PrivateDSFnC" } +'@ | Out-File -FilePath $dotSourceFilePathC + @' + . {0} + function PublicFnC {{ "PublicFnC"; PublicDSFnC }} + function PrivateFnC {{ "PrivateFnC"; PrivateDSFnC }} + + Export-ModuleMember -Function "*" +'@ -f $dotSourceFilePathC | Out-File -FilePath $moduleFilePathC + '@{{ ModuleVersion = "1.0"; RootModule = "{0}"; FunctionsToExport = @("PublicFnC","PublicDSFnC") }}' -f $moduleFilePathC | Out-File -FilePath $manifestFilePathC + + # Module that dot sources ps1 file while and exports functions with no wildcards. + $scriptModuleNameD = "ModuleDotSourceNoWildcard_System32" + $moduleFilePathD = Join-Path $TestModulePath ($scriptModuleNameD + ".psm1") + $dotSourceNameD = "DotSourceFileNoWildCardD_System32" + $dotSourceFilePathD = Join-Path $TestModulePath ($dotSourceNameD + ".ps1") + @' + function PublicDSFnD { "PublicDSFnD"; PrivateDSFnD } + function PrivateDSFnD { "PrivateDSFnD" } +'@ | Out-File -FilePath $dotSourceFilePathD + @' + . {0} + function PublicFnD {{ "PublicFnD"; PublicDSFnD }} + function PrivateFnD {{ "PrivateFnD"; PrivateDSFnD }} + + Export-ModuleMember -Function "PublicFnD","PublicDSFnD" +'@ -f $dotSourceFilePathD | Out-File -FilePath $moduleFilePathD + + # Module that dot sources ps1 file but does not use Export-ModuleMember + $scriptModuleNameE = "ModuleDotSourceNoExportE_System32" + $moduleFilePathE = Join-Path $TestModulePath ($scriptModuleNameE + ".psm1") + $dotSourceNameE = "DotSourceFileNoExportE_System32" + $dotSourceFilePathE = Join-Path $TestModulePath ($dotSourceNameE + ".ps1") + @' + function PublicDSFnE { "PublicDSFnE"; PrivateDSFnE } + function PrivateDSFnE { "PrivateDSFnE" } +'@ | Out-File -FilePath $dotSourceFilePathE + @' + . {0} + function PublicFnE {{ "PublicFnE"; PublicDSFnE }} + function PrivateFnE {{ "PrivateFnE"; PrivateDSFnE }} +'@ -f $dotSourceFilePathE | Out-File -FilePath $moduleFilePathE + + # Module with dot source ps1 file and nested modules that do use Export-ModuleMember + $scriptModuleNameF = "ModuleDotSourceNestedExport_System32" + $moduleFilePathF = Join-Path $TestModulePath ($scriptModuleNameF + ".psm1") + $manifestFilePathF = Join-Path $TestModulePath ($scriptModuleNameF + ".psd1") + $nestedSourceNameF = "NestedSourceWithExport_System32" + $nestedSourceFilePathF = Join-Path $TestModulePath ($nestedSourceNameF + ".psm1") + @' + . {0} + function NestedPubFnF {{ "NestedPubFnF"; PublicDSFnE }} + + Export-ModuleMember -Function * +'@ -f $dotSourceFilePathE | Out-File -FilePath $nestedSourceFilePathF + @' + function PublicFnF { "PublicFnF"; NestedPubFnF } +'@ | Out-File -FilePath $moduleFilePathF + '@{{ ModuleVersion = "1.0"; RootModule = "{0}"; NestedModules = "{1}"; FunctionsToExport = "PublicFnF","NestedPubFnF" }}' -f $moduleFilePathF,$nestedSourceFilePathF | Out-File -FilePath $manifestFilePathF + + # Module with dot source ps1 file and import module and Export-ModuleMember with wildcard + $scriptModuleNameG = "ModuleDotSourceImportExport_System32" + $moduleFilePathG = Join-Path $TestModulePath ($scriptModuleNameG + ".psm1") + $importModNameG = "ImportModWitExport_System32" + $importModFilePathG = Join-Path $TestModulePath ($importModNameG + ".psm1") + @' + . {0} + function ImportPubFnG {{ "ImportPubFnG"; PublicDSFnE }} + + Export-ModuleMember -Function * +'@ -f $dotSourceFilePathE | Out-File $importModFilePathG + @' + Import-Module {0} + function PublicFnG {{ "PublicFnG"; ImportPubFnG }} + + Export-ModuleMember -Function PublicFnG +'@ -f $importModFilePathG | Out-File -FilePath $moduleFilePathG + + # Module with dot source and with multiple Export-ModuleMember use. + $scriptModuleNameH = "ModuleDotSourceImportExportH_System32" + $moduleFilePathH = Join-Path $TestModulePath ($scriptModuleNameH + ".psm1") + @' + . {0} + function PublicFnH {{ "PublicFnH"; PrivateFnH }} + function PrivateFnH {{ "PrivateFnH" }} + + Export-ModuleMember -Function * + Export-ModuleMember -Function PublicFnH +'@ -f $dotSourceFilePathE | Out-File $moduleFilePathH + + # Module with dot source and only class definition, and no functions exported. + $scriptModuleNameI = "ModuleDotSourceClassesOnly_System32" + $moduleFilePathI = Join-Path $TestModulePath ($scriptModuleNameI + ".psm1") + @' + class Class1 {{ static [string] GetMessage() {{ . {0}; return "Message" }} }} +'@ -f $dotSourceFilePathE | Out-File $moduleFilePathI + + # Module manifest with dot source and only class definition, and no functions exported. + $scriptManifestNameI = "ManifestDotSourceClassesOnly_System32" + $moduleManifestPathI = Join-Path $TestModulePath ($scriptManifestNameI + ".psd1") + "@{ ModuleVersion='1.0'; RootModule='$moduleFilePathI' }" | Out-File $moduleManifestPathI + + # Module with using directive + $sriptModuleNameJ = "ModuleWithUsing_System32" + $moduleFilePathJ = Join-Path $TestModulePath ($sriptModuleNameJ + ".psm1") + @' + using module {0} + function PublicUsingFn {{ [Class1]::GetMessage() }} + Export-ModuleMember -Function PublicUsingFn +'@ -f $moduleManifestPathI | Out-File $moduleFilePathJ + + Write-Verbose "Test module files created" + } + + It "Verifies that importing trusted module in system lockdown which dot sources a ps1 file while exporting all functions with wildcard throws expected error" { + + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + Import-Module -Name $moduleFilePathA -Force 2>$null + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "Modules_SystemLockDown_CannotUseDotSourceWithWildCardFunctionExport,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Verifies that importing trusted module in system lockdown which dot sources a ps1 file that exports functions with wildcard throws expected error" { + + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + Import-Module -Name $moduleFilePathB -Force 2>$null + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "Modules_SystemLockDown_CannotUseDotSourceWithWildCardFunctionExport,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Verifies that importing trusted module in system lockdown which dot sources a ps1 file while exporting functions with wildcard but has overriding manifest export does not throw error" { + + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + $module = Import-Module -Name $manifestFilePathC -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 2 + $module.ExportedCommands["PublicFnC"] | Should -Not -BeNullOrEmpty + $module.ExportedCommands["PublicDSFnC"] | Should -Not -BeNullOrEmpty + } + + It "Verifies that importing trusted module in system lockdown which dot sources ps1 file but does not export functions with wildcard does not throw error" { + + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + $module = Import-Module -Name $moduleFilePathD -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 2 + $module.ExportedCommands["PublicFnD"] | Should -Not -BeNullOrEmpty + $module.ExportedCommands["PublicDSFnD"] | Should -Not -BeNullOrEmpty + } + + It "Verifies that importing trusted module with dotsource and wildcard function export works when not in lock down mode" { + + $module = Import-Module -Name $moduleFilePathA -Force -PassThru + + $module.ExportedCommands.Count | Should -Be 4 + $module.ExportedCommands["PublicFnA"] | Should -Not -BeNullOrEmpty + $module.ExportedCommands["PrivateFnA"] | Should -Not -BeNullOrEmpty + $module.ExportedCommands["PublicDSFnA"] | Should -Not -BeNullOrEmpty + $module.ExportedCommands["PrivateDSFnA"] | Should -Not -BeNullOrEmpty + } + + It "Verifies that importing trusted module with dotsource and no function export works without error in lockdown mode" { + + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + $module = Import-Module -Name $moduleFilePathE -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 0 + } + + It "Verifies that dot source manifest and module with nested module works as expected in system lock down" { + + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + $module = Import-Module -Name $manifestFilePathF -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 2 + $module.ExportedCommands["PublicFnF"] | Should -Not -BeNullOrEmpty + $module.ExportedCommands["NestedPubFnF"] | Should -Not -BeNullOrEmpty + } + + It "Verifies that an imported module that dot sources and exports via wilcard is detected and disallowed" { + + try + { + $expectedError = $null + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + $module = Import-Module -Name $moduleFilePathG -Force -PassThru -ErrorVariable expectedError 2>$null + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $expectedError[0].FullyQualifiedErrorId | Should -BeExactly "Modules_SystemLockDown_CannotUseDotSourceWithWildCardFunctionExport,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Verifies that a module with dot source file and multiple Export-ModuleMember calls still errors with wildcard" { + + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + $module = Import-Module -Name $moduleFilePathH -Force -PassThru 2>$null + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "Modules_SystemLockDown_CannotUseDotSourceWithWildCardFunctionExport,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Verifies that a classes only module with dot-source and with using directive loads successfully" { + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + $module = Import-Module -Name $moduleFilePathJ -Force -PassThru + $result = PublicUsingFn + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module | Should -Not -BeNullOrEmpty + $result | Should -BeExactly "Message" + } + } + + Describe "Call operator invocation of trusted module private function" -Tags 'Feature','RequireAdminOnWindows' { + + BeforeAll { + + $scriptModuleName = "ImportTrustedManifestWithCallOperator_System32" + $moduleFileName = Join-Path $TestDrive ($scriptModuleName + ".psm1") + $manifestFileName = Join-Path $TestDrive ($scriptModuleName + ".psd1") + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName'; FunctionsToExport = 'PublicFn' }" > $manifestFileName + } + + It "Verifies expected error when call operator attempts to access trusted module scope function" { + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + + $module = Import-Module -Name $manifestFileName -Force -PassThru + + & $module PrivateFn + + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "CantInvokeCallOperatorAcrossLanguageBoundaries" + } + } + + Describe "Tests module table restrictions" -Tags 'Feature','RequireAdminOnWindows' { + + BeforeAll { + + # Module directory + $moduleName = "Modules_" + (Get-RandomFileName) + $modulePath = Join-Path $TestDrive $moduleName + New-Item -ItemType Directory -Path $modulePath + + # Parent module directory + $scriptModuleName = "TrustedParentModule_System32" + $scriptModulePath = Join-Path $modulePath $scriptModuleName + $moduleFileName = Join-Path $scriptModulePath ($scriptModuleName + ".psm1") + $manifestFileName = Join-Path $scriptModulePath ($scriptModuleName + ".psd1") + New-Item -ItemType Directory -Path $scriptModulePath + + # Import module directory + $scriptModuleImportName = "TrustedImportModule_System32" + $scriptModuleImportPath = Join-Path $modulePath $scriptModuleImportName + $moduleImportFileName = Join-Path $scriptModuleImportPath ($scriptModuleImportName + ".psm1") + New-Item -ItemType Directory -Path $scriptModuleImportPath + + @' + Import-Module -Name {0} + + function PublicFn + {{ + Write-Host "" + Write-Host "PublicFn" + PrivateFn1 + }} +'@ -f $scriptModuleImportName > $moduleFileName + + @' + function PrivateFn1 + { + Write-Host "" + Write-Host "PrivateFn1" + Write-Host "Language mode: $($ExecutionContext.SessionState.LanguageMode)" + } +'@ > $moduleImportFileName + + "@{ ModuleVersion = '1.0'; NestedModules = '$moduleFileName'; FunctionsToExport = 'PublicFn' }" > $manifestFileName + + $savedPSModulePath = $env:PSModulePath + $env:PSModulePath += (";" + $modulePath) + } + + AfterAll { + + if ($savedPSModulePath -ne $null) { $env:PSModulePath = $savedPSModulePath } + } + + It "Verifies that Get-Command does not expose private module function under system lock down" { + + $GetCommandPublicFnCmdInfo = $null + $GetCommandPrivateFnCmdInfo = $null + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + + # Imports both TrustedParentModule_System32 and TrustedImportModule_System32 modules + Import-Module -Name $scriptModuleName -Force + + # Public functions should be available in the session + $GetCommandPublicFnCmdInfo = Get-Command -Name "PublicFn" 2>$null + + # Private functions should not be available in the session + # Get-Command will import the TrustedImportModule_System32 module from the PSModulePath to find PrivateFn1 + # However, it should not get TrustedImportModule_System32 from the module cache because it was loaded in a + # different language mode, and should instead re-load it (equivalent to Import-Module -Force) + $GetCommandPrivateFnCmdInfo = Get-Command -Name "PrivateFn1" 2>$null + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $GetCommandPublicFnCmdInfo | Should -Not -BeNullOrEmpty + $GetCommandPrivateFnCmdInfo | Should -BeNullOrEmpty + } + + It "Verifies that Get-Command does not expose private function after explicitly importing nested module file under system lock down" { + + $ReImportPublicFnCmdInfo = $null + $ReImportPrivateFnCmdInfo = $null + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + + # Imports both TrustedParentModule_System32 and TrustedImportModule_System32 modules + Import-Module -Name $scriptModuleName -Force + + # Directly import nested TrustedImportModule_System32 module. + # This makes TrustedImportModule_System32 functions visible but should not use the existing loaded module + # since all functions are visible, but instead should re-load the module with the correct language context, + # ensuring only explictly exported functions are visible. + Import-Module -Name $scriptModuleImportName + + # Public functions should be available in the session + $ReImportPublicFnCmdInfo = Get-Command -Name "PublicFn" 2>$null + + # Private functions should not be available in the session + $ReImportPrivateFnCmdInfo = Get-Command -Name "PrivateFn1" 2>$null + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $ReImportPublicFnCmdInfo | Should -Not -BeNullOrEmpty + $ReImportPrivateFnCmdInfo | Should -BeNullOrEmpty + } + } + + Describe "Import mix of trusted and untrusted manifest and module files" -Tags 'Feature','RequireAdminOnWindows' { + + It "Verifies that an untrusted manifest with a trusted module will not load under system lockdown" { + + $manifestFileName = Join-Path $TestDrive "ImportUnTrustedManifestWithFnExport.psd1" + $moduleFileName = Join-Path $TestDrive "ImportUnTrustedManifestWithFnExport_System32.psm1" + + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName'; FunctionsToExport = 'PublicFn','PrivateFn' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + + Import-Module -Name $manifestFileName -Force -ErrorAction Stop + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "Modules_MismatchedLanguageModes,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Verifies that an untrusted manifest with a trusted binary module does load under system lockdown" { + + $modulePath = "$PSScriptRoot\Modules" + New-Item -Path $modulePath -ItemType Directory -Force + + $manifestFileName = Join-Path $modulePath "ImportUnTrustedManifestWithBinFnExport.psd1" + $moduleFileName = Join-Path $modulePath "ImportUnTrustedManifestWithBinFnExport_System32.dll" + "@{ ModuleVersion = '1.0'; NestedModules = '$moduleFileName'; CmdletsToExport = 'Invoke-Hello' }" > $manifestFileName + + $code = @' + using System; + using System.Management.Automation; + + [Cmdlet("Invoke", "Hello")] + public sealed class InvokeHello : PSCmdlet + { + protected override void EndProcessing() + { + System.Console.WriteLine("Hello!"); + } + } +'@ + try { Add-Type -TypeDefinition $code -OutputAssembly $moduleFileName -ErrorAction Ignore } catch {} + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module | Should -Not -BeNullOrEmpty + $module.ExportedCommands["Invoke-Hello"] | Should -Not -BeNullOrEmpty + + if ($module -ne $null) { Remove-Module -Name $module.Name -Force -ErrorAction Ignore } + } + + It "Verifies that an untrusted module with nested trusted modules cannot load in a locked down system" { + + $manifestFileName = Join-Path $TestDrive "ImportUnTrustedManifestWithTrustedModule.psd1" + $moduleFileName = Join-Path $TestDrive "ImportUnTrustedManifestWithTrustedModule_System32.psm1" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + + "@{ ModuleVersion = '1.0'; NestedModules = '$moduleFileName'; FunctionsToExport = 'PublicFn','PrivateFn' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + + Import-Module -Name $manifestFileName -Force -ErrorAction Stop + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "Modules_MismatchedLanguageModes,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "Verifies that an untrusted manifest containing all trusted modules does not load under system lock down" { + + $moduleFileName1 = Join-Path $TestDrive "ImportUnTrustedManifestWithTrustedModules1_System32.psm1" + $moduleFileName2 = Join-Path $TestDrive "ImportUnTrustedManifestWithTrustedModules2_System32.psm1" + $manifestFileName = Join-Path $TestDrive "ImportUnTrustedManifestWithTrustedModules.psd1" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName1 + @' + function PublicFn2 + { + Write-Output "PublicFn2" + } +'@ > $moduleFileName2 + + "@{ ModuleVersion = '1.0'; NestedModules = '$moduleFileName1'; RootModule = '$moduleFileName2'; FunctionsToExport = 'PublicFn','PrivateFn' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + + Import-Module -Name $manifestFileName -Force -ErrorAction Stop + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "Modules_MismatchedLanguageModes,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + # End Describe Block + } + + Describe "Import trusted module files in system lockdown mode" -Tags 'Feature','RequireAdminOnWindows' { + + function CreateModuleNames + { + param ( + [string] $moduleName + ) + + $script:scriptModuleName = $moduleName + $script:moduleFileName = Join-Path $TestDrive ($moduleName + ".psm1") + } + + It "Verifes that trusted module file exports no functions in system lockdown" { + + CreateModuleNames "ImportTrustedModuleWithNoFnExport_System32" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $moduleFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 0 + } + + It "Verifies that trusted module file exports only exported function in system lockdown" { + + CreateModuleNames "ImportTrustedModuleWithFnExport_System32" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } + Export-ModuleMember -Function PublicFn +'@ > $moduleFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $moduleFileName -Force -PassThr + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 1 + $module.ExportedCommands.Values[0].Name | Should -BeExactly "PublicFn" + } + + It "Verifies that trusted module with wild card function export in system lockdown" { + + CreateModuleNames "ImportTrustedModuleWithWildcardFnExport_System32" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } + Export-ModuleMember -Function * +'@ > $moduleFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $moduleFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 2 + } + } + + Describe "Import trusted manifest files in system lockdown mode" -Tags 'Feature','RequireAdminOnWindows' { + + function CreateManifestNames + { + param ( + [string] $moduleName, + [switch] $twoModules, + [switch] $noExtension, + [switch] $dotSourceModule + ) + + $script:scriptModuleName = $moduleName + $script:moduleFileName = Join-Path $TestDrive ($moduleName + ".psm1") + $script:manifestFileName = Join-Path $TestDrive ($moduleName + ".psd1") + if ($twoModules) + { + $script:moduleFileName2 = Join-Path $TestDrive ($moduleName + "2.psm1") + } + if ($noExtension) + { + $script:moduleFileNameNoExt = Join-Path $TestDrive $scriptModuleName + } + if ($dotSourceModule) + { + $script:dotmoduleFileName = Join-Path $TestDrive ($moduleName + "Dot" + ".ps1") + } + } + + It "Verifies that trusted manifest exports no functions by default in lock down mode" { + + CreateManifestNames "ImportTrustedManifestWithNoFnExport_System32" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 0 + } + + It "Verifies that trusted manifest exports no functions through wildcard in lock down mode" { + + CreateManifestNames "ImportTrustedManifestWithWildcardFnExport1_System32" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName'; FunctionsToExport = '*' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 0 + } + + It "Verifies that trusted manifest exports no functions through name wildcard in lock down mode" { + + CreateManifestNames "ImportTrustedManifestWithWildcardNameFnExport_System32" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName'; FunctionsToExport = '*Fn*' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 0 + } + + It "Verifies that trusted manifest exports a single module function and ignores wildcard in lock down mode" { + + CreateManifestNames "ImportTrustedManifestWithWildcardModFnExport_System32" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } + + Export-ModuleMember -Function "PublicFn" +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName'; FunctionsToExport = '*' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 1 + $module.ExportedCommands.Values[0].Name | Should -BeExactly "PublicFn" + } + + It "Verifies that trusted manifest exports no functions through the cmdlets export keyword" { + + CreateManifestNames "ImportTrustedManifestWithCmdletExport_System32" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName'; CmdletsToExport = '*' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 0 + } + + It "Verifies that trusted manifest with wildcard exports a single function from two modules" { + + CreateManifestNames "ImportTrustedManifestWithTwoMods_System32" -TwoModules + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } + Export-ModuleMember -Function PublicFn +'@ > $moduleFileName + @' + function PrivateFn3 + { + Write-Output "PublicFn" + } + + function PrivateFn4 + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName2 + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName'; NestedModules = '$moduleFileName2'; FunctionsToExport = '*' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 1 + $module.ExportedCommands.Values[0].Name | Should -BeExactly "PublicFn" + } + + It "Verifies that trusted manifest explicitly exports a single function" { + + CreateManifestNames "ImportTrustedManifestWithExportFn_System32" + + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName'; FunctionsToExport = 'PublicFn' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 1 + $module.ExportedCommands.Values[0].Name | Should -BeExactly "PublicFn" + } + + It "Verifies that trusted manifest with nested modules exports explicit function" { + + CreateManifestNames "ImportTrustedManifestWithNestedModsAndFnExport_System32" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; NestedModules = '$moduleFileName'; FunctionsToExport = 'PublicFn' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 1 + $module.ExportedCommands.Values[0].Name | Should -BeExactly "PublicFn" + } + + It "Verifies that trusted manifest with nested modules exports no functions by default" { + + CreateManifestNames "ImportTrustedManifestWithNestedModsAndNoFnExport_System32" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; NestedModules = '$moduleFileName' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 0 + } + + It "Verifies that trusted manifest with nested modules and no extension module exports explicit function" { + + CreateManifestNames "ImportTrustedManifestWithNestedModsAndNoExtNoFnExport_System32" -NoExtension + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; NestedModules = '$moduleFileNameNoExt'; FunctionsToExport = 'PublicFn' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 1 + $module.ExportedCommands.Values[0].Name | Should -BeExactly "PublicFn" + } + + It "Verifies that trusted manifest with dot source module file respects lock down mode" { + + CreateManifestNames "ImportTrustedManifestWithDotSourceModAndFnExport_System32" -DotSourceModule + @' + function PublicFn + { + Write-Output "PublicFn" + } + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $dotmoduleFileName + @' + . {0} + + function PrivateFn1 + {{ + Write-Output "PrivateFn1" + + }} + + function PrivateFn2 + {{ + Write-Output "PrivateFn2" + }} +'@ -f $dotmoduleFileName > $moduleFileName + "@{ ModuleVersion = '1.0'; NestedModules = '$moduleFileName'; FunctionsToExport = 'PublicFn' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 1 + $module.ExportedCommands.Values[0].Name | Should -BeExactly "PublicFn" + } + } + + Describe "Untrusted manifest and module files import in lock down mode" -Tags 'Feature','RequireAdminOnWindows' { + + function CreateManifestNames + { + param ( + [string] $moduleName + ) + + $script:scriptModuleName = $moduleName + $script:moduleFileName = Join-Path $TestDrive ($moduleName + ".psm1") + $script:manifestFileName = Join-Path $TestDrive ($moduleName + ".psd1") + } + + It "Verifies that importing untrusted manifest in lock down mode exports all functions by default" { + + CreateManifestNames "ImportUntrustedManifestWithNoFnExport" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 2 + } + + It "Verifies that importing untrusted manifest in lock down mode exports explicit function" { + + CreateManifestNames "ImportUntrustedManifestWithFnExport" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + "@{ ModuleVersion = '1.0'; RootModule = '$moduleFileName'; FunctionsToExport = 'PrivateFn' }" > $manifestFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $manifestFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 1 + $module.ExportedCommands.Values[0].Name | Should -BeExactly 'PrivateFn' + } + + It "Verifies that importing untrusted module file in lock down mode exports all functions by default" { + + CreateManifestNames "ImportUnTrustedModuleWithNoFnExport" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } +'@ > $moduleFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $moduleFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 2 + } + + It "Verifies that importing untrusted module file in lock down mode exports explicit function" { + + CreateManifestNames "ImportUnTrustedModuleWithFnExport" + @' + function PublicFn + { + Write-Output "PublicFn" + } + + function PrivateFn + { + Write-Output "PrivateFn" + } + Export-ModuleMember -Function PublicFn +'@ > $moduleFileName + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $module = Import-Module -Name $moduleFileName -Force -PassThru + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $module.ExportedCommands.Count | Should -Be 1 + $module.ExportedCommands.Values[0].Name | Should -BeExactly 'PublicFn' + } + } +} +finally +{ + if ($defaultParamValues -ne $null) + { + $Global:PSDefaultParameterValues = $defaultParamValues + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 index ffd388b6f73..117c21aebb1 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 @@ -65,17 +65,30 @@ try Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode } - $expectedErrorId | Should BeExactly "MethodInvocationNotSupportedInConstrainedLanguage" + $expectedErrorId | Should -BeExactly "MethodInvocationNotSupportedInConstrainedLanguage" } } Context "Background jobs within inconsistent mode" { It "Verifies that background job is denied when mode is inconsistent" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - { Start-Job { [object]::Equals("A", "B") } } | Should -Throw -ErrorId "CannotStartJobInconsistentLanguageMode" - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Start-Job { [object]::Equals("A", "B") } + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotStartJobInconsistentLanguageMode,Microsoft.PowerShell.Commands.StartJobCommand" } } } @@ -83,15 +96,28 @@ try Describe "Add-Type in constrained language" -Tags 'Feature','RequireAdminOnWindows' { It "Verifies Add-Type fails in constrained language mode" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - { Add-Type -TypeDefinition 'public class ConstrainedLanguageTest { public static string Hello = "HelloConstrained"; }' } | - Should -Throw -ErrorId "CannotDefineNewType" - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Add-Type -TypeDefinition 'public class ConstrainedLanguageTest { public static string Hello = "HelloConstrained"; }' + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotDefineNewType,Microsoft.PowerShell.Commands.AddTypeCommand" } It "Verifies Add-Type works back in full language mode again" { Add-Type -TypeDefinition 'public class AfterFullLanguageTest { public static string Hello = "HelloAfter"; }' - [AfterFullLanguageTest]::Hello | Should -Be "HelloAfter" + [AfterFullLanguageTest]::Hello | Should -BeExactly "HelloAfter" } } @@ -116,10 +142,23 @@ try } It "Verifies New-Object throws error in constrained language for disallowed IntPtr type" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - { New-Object System.IntPtr 1234 } | Should -Throw -ErrorId "CannotCreateTypeConstrainedLanguage" - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + New-Object System.IntPtr 1234 + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotCreateTypeConstrainedLanguage,Microsoft.PowerShell.Commands.NewObjectCommand" } It "Verifies New-Object works for IntPtr type back in full language mode again" { @@ -131,12 +170,25 @@ try Context "New-Object with COM types" { It "Verifies New-Object with COM types is disallowed in system lock down" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode - { New-Object -Com ADODB.Parameter } | Should -Throw -ErrorId "CannotCreateComTypeConstrainedLanguage" + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode - Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + New-Object -Com ADODB.Parameter + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotCreateComTypeConstrainedLanguage,Microsoft.PowerShell.Commands.NewObjectCommand" } It "Verifies New-Object with COM types works back in full language mode again" { @@ -150,22 +202,48 @@ try Describe "New-Item command on function drive in constrained language" -Tags 'Feature','RequireAdminOnWindows' { It "Verifies New-Item directory on function drive is not allowed in constrained language mode" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - { $null = New-Item -Path function:\SomeEvilFunction -ItemType Directory -Value SomeBadScriptBlock -ErrorAction Stop } | - Should -Throw -ErrorId "NotSupported" - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + $null = New-Item -Path function:\SomeEvilFunction -ItemType Directory -Value SomeBadScriptBlock -ErrorAction Stop + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "NotSupported,Microsoft.PowerShell.Commands.NewItemCommand" } } Describe "Script debugging in constrained language" -Tags 'Feature','RequireAdminOnWindows' { It "Verifies that a debugging breakpoint cannot be set in constrained language and no system lockdown" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - function MyDebuggerFunction {} - { Set-PSBreakpoint -Command MyDebuggerFunction } | Should -Throw -ErrorId "CannotSetBreakpointInconsistentLanguageMode" + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + function MyDebuggerFunction {} + + Set-PSBreakpoint -Command MyDebuggerFunction + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotSetBreakpointInconsistentLanguageMode,Microsoft.PowerShell.Commands.SetPSBreakpointCommand" } It "Verifies that a debugging breakpoint can be set in constrained language with system lockdown" { @@ -185,23 +263,36 @@ try Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode } - $Global:DebuggingOk | Should -Be "DebuggingOk" + $Global:DebuggingOk | Should -BeExactly "DebuggingOk" } It "Verifies that debugger commands do not run in full language mode when system is locked down" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - function MyDebuggerFunction3 {} + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + function MyDebuggerFunction3 {} + & { + $null = Set-PSBreakpoint -Command MyDebuggerFunction3 -Action { $Global:dbgResult = [object]::Equals("A", "B") } + $restoreEAPreference = $ErrorActionPreference + $ErrorActionPreference = "Stop" + MyDebuggerFunction3 + } + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally { - $null = Set-PSBreakpoint -Command MyDebuggerFunction3 -Action { $Global:dbgResult = [object]::Equals("A", "B") } - $restoreEAPreference = $ErrorActionPreference - $ErrorActionPreference = "Stop" - MyDebuggerFunction3 - } | Should -Throw -ErrorId "CannotSetBreakpointInconsistentLanguageMode" + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + if ($restoreEAPreference -ne $null) { $ErrorActionPreference = $restoreEAPreference } + } - if ($restoreEAPreference -ne $null) { $ErrorActionPreference = $restoreEAPreference } - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotSetBreakpointInconsistentLanguageMode,Microsoft.PowerShell.Commands.SetPSBreakpointCommand" } It "Verifies that debugger command injection is blocked in system lock down" { @@ -269,10 +360,21 @@ try Import-Module PSDiagnostics $module = Get-Module PSDiagnostics - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - { & $module { [object]::Equals("A", "B") } } | Should -Throw -ErrorId "MethodInvocationNotSupportedInConstrainedLanguage" + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + & $module { [object]::Equals("A", "B") } + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + $expectedError.FullyQualifiedErrorId | Should -BeExactly "CantInvokeCallOperatorAcrossLanguageBoundaries" } } @@ -344,35 +446,73 @@ try param ($scriptblock) - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - { & $scriptblock } | Should -Throw -ErrorId "MethodInvocationNotSupportedInConstrainedLanguage" + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + & $scriptblock + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + $expectedError.FullyQualifiedErrorId | Should -BeExactly "MethodInvocationNotSupportedInConstrainedLanguage,Microsoft.PowerShell.Commands.InvokeExpressionCommand" } } Describe "Dynamic method invocation in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' { It "Verifies dynamic method invocation does not bypass constrained language mode" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + try { - $type = [IO.Path] - $method = "GetRandomFileName" - $type::$method() - } | Should -Throw -ErrorId "MethodInvocationNotSupportedInConstrainedLanguage" + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + & { + $type = [IO.Path] + $method = "GetRandomFileName" + $type::$method() + } + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + $expectedError.FullyQualifiedErrorId | Should -BeExactly "MethodInvocationNotSupportedInConstrainedLanguage" } It "Verifies dynamic methods invocation does not bypass constrained language mode" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + try { - $type = [IO.Path] - $methods = "GetRandomFileName","GetTempPath" - $type::($methods[0])() - } | Should -Throw -ErrorId "MethodInvocationNotSupportedInConstrainedLanguage" + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + & { + $type = [IO.Path] + $methods = "GetRandomFileName","GetTempPath" + $type::($methods[0])() + } + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + $expectedError.FullyQualifiedErrorId | Should -BeExactly "MethodInvocationNotSupportedInConstrainedLanguage" } } @@ -398,19 +538,43 @@ try Describe "Variable AllScope in constrained language mode" -Tags 'Feature','RequireAdminOnWindows' { It "Verifies Set-Variable cannot create AllScope in constrained language" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - { Set-Variable -Name SetVariableAllScopeNotSupported -Value bar -Option AllScope } | - Should -Throw -ErrorId "NotSupported" - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + Set-Variable -Name SetVariableAllScopeNotSupported -Value bar -Option AllScope + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "NotSupported,Microsoft.PowerShell.Commands.SetVariableCommand" } It "Verifies New-Variable cannot create AllScope in constrained language" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - { New-Variable -Name NewVarialbeAllScopeNotSupported -Value bar -Option AllScope } | - Should -Throw -ErrorId "NotSupported" - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + New-Variable -Name NewVarialbeAllScopeNotSupported -Value bar -Option AllScope + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "NotSupported,Microsoft.PowerShell.Commands.NewVariableCommand" } } @@ -418,9 +582,15 @@ try function InvokeDataSectionConstrained { - $e = { Invoke-Expression 'data foo -SupportedCommand Add-Type { Add-Type }' } | Should -Throw -PassThru - - return $e + try + { + Invoke-Expression 'data foo -SupportedCommand Add-Type { Add-Type }' + throw "No Exception!" + } + catch + { + return $_ + } } It "Verifies data section Add-Type additional command is disallowed in constrained language" { @@ -443,26 +613,50 @@ try } It "Verifies data section with no-constant expression Add-Type additional command is disallowed in constrained language" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" - $addedCommand = "Add-Type" - { Invoke-Expression 'data foo -SupportedCommand $addedCommand { Add-Type }' } | - Should -Throw -ErrorId "DataSectionAllowedCommandDisallowed" - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + $addedCommand = "Add-Type" + Invoke-Expression 'data foo -SupportedCommand $addedCommand { Add-Type }' + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } + + $expectedError.FullyQualifiedErrorId | Should -BeExactly "DataSectionAllowedCommandDisallowed,Microsoft.PowerShell.Commands.InvokeExpressionCommand" } } Describe "Import-LocalizedData additional commands in constrained language" -Tags 'Feature','RequireAdminOnWindows' { It "Verifies Import-LocalizedData disallows Add-Type in constrained language" { - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + try { - $localizedDataFileName = Join-Path $TestDrive ImportLocalizedDataAdditionalCommandsNotSupported.psd1 - $null = New-Item -ItemType File -Path $localizedDataFileName -Force - Import-LocalizedData -SupportedCommand Add-Type -BaseDirectory $TestDrive -FileName ImportLocalizedDataAdditionalCommandsNotSupported - } | Should -Throw -ErrorId "CannotDefineSupportedCommand" + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + & { + $localizedDataFileName = Join-Path $TestDrive ImportLocalizedDataAdditionalCommandsNotSupported.psd1 + $null = New-Item -ItemType File -Path $localizedDataFileName -Force + Import-LocalizedData -SupportedCommand Add-Type -BaseDirectory $TestDrive -FileName ImportLocalizedDataAdditionalCommandsNotSupported + } + } + catch + { + $expectedError = $_ + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + $expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotDefineSupportedCommand,Microsoft.PowerShell.Commands.ImportLocalizedData" } } @@ -551,14 +745,27 @@ try param ( [string] $script ) - $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + try + { + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + & { + # Scriptblock must be created inside constrained language. + $sb = [scriptblock]::Create($script) + & sb + } + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + finally { - # Scriptblock must be created inside constrained language. - $sb = [scriptblock]::Create($script) - & sb - } | Should -Throw -ErrorId "MethodInvocationNotSupportedInConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + } - Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode + $expectedError.FullyQualifiedErrorId | Should -BeExactly "MethodInvocationNotSupportedInConstrainedLanguage" } } @@ -577,12 +784,13 @@ try Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode $results = Start-ThreadJob -ScriptBlock { $ExecutionContext.SessionState.LanguageMode } | Wait-Job | Receive-Job - $results | Should BeExactly "ConstrainedLanguage" } finally { Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode } + + $results | Should -BeExactly "ConstrainedLanguage" } It "ThreadJob script block using variable must run in ConstrainedLanguage mode with system lock down" { @@ -593,12 +801,13 @@ try Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode $results = Start-ThreadJob -ScriptBlock { & $using:sb } | Wait-Job | Receive-Job - $results | Should BeExactly "ConstrainedLanguage" } finally { Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode } + + $results | Should -BeExactly "ConstrainedLanguage" } It "ThreadJob script block argument variable must run in ConstrainedLanguage mode with system lock down" { @@ -609,12 +818,13 @@ try Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode $results = Start-ThreadJob -ScriptBlock { param ($sb) & $sb } -ArgumentList $sb | Wait-Job | Receive-Job - $results | Should BeExactly "ConstrainedLanguage" } finally { Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode } + + $results | Should -BeExactly "ConstrainedLanguage" } It "ThreadJob script block piped variable must run in ConstrainedLanguage mode with system lock down" { @@ -625,12 +835,13 @@ try Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode $results = $sb | Start-ThreadJob -ScriptBlock { $input | foreach { & $_ } } | Wait-Job | Receive-Job - $results | Should BeExactly "ConstrainedLanguage" } finally { Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode } + + $results | Should -BeExactly "ConstrainedLanguage" } } From 60a6cd75682e164444d1e7c3354b5577656c8840 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Thu, 4 Oct 2018 10:41:24 -0700 Subject: [PATCH 05/13] Untrusted input tracking work --- .../commands/utility/AddType.cs | 5 + .../commands/utility/Import-LocalizedData.cs | 8 + .../commands/utility/InvokeCommandCmdlet.cs | 1 + .../commands/utility/Var.cs | 10 + .../commands/utility/new-object.cs | 4 + .../engine/ArgumentTypeConverterAttribute.cs | 7 + .../engine/Attributes.cs | 51 ++ .../engine/CmdletInfo.cs | 3 + .../CommandCompletion/CompletionAnalysis.cs | 2 +- .../CommandCompletion/CompletionCompleters.cs | 2 +- .../PseudoParameterBinder.cs | 2 +- .../engine/CommandProcessor.cs | 39 +- .../engine/ExecutionContext.cs | 136 ++++- .../engine/InternalCommands.cs | 2 + .../engine/LanguagePrimitives.cs | 7 +- .../engine/Modules/ImportModuleCommand.cs | 4 + .../engine/ParameterBinderBase.cs | 47 +- .../engine/ScriptCommandProcessor.cs | 21 +- .../engine/SessionStateScope.cs | 43 +- .../engine/ShellVariable.cs | 2 +- .../engine/VariableAttributeCollection.cs | 2 +- .../engine/parser/TypeResolver.cs | 1 + .../engine/remoting/commands/StartJob.cs | 7 + .../engine/runtime/Operations/MiscOps.cs | 11 + .../engine/runtime/Operations/VariableOps.cs | 7 + .../resources/Metadata.resx | 3 + .../Parser/ParameterBinding.Tests.ps1 | 326 ++++++++++ .../Security/UntrustedDataMode.Tests.ps1 | 560 ++++++++++++++++++ 28 files changed, 1254 insertions(+), 59 deletions(-) create mode 100644 test/powershell/engine/Security/UntrustedDataMode.Tests.ps1 diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs index 8184c065a84..6e8486df885 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs @@ -74,6 +74,7 @@ public sealed class AddTypeCommand : PSCmdlet /// The source code of this generated type. /// [Parameter(Mandatory = true, Position = 0, ParameterSetName = FromSourceParameterSetName)] + [ValidateTrustedData] public String TypeDefinition { get @@ -90,6 +91,7 @@ public String TypeDefinition /// The name of the type (class) used for auto-generated types. /// [Parameter(Mandatory = true, Position = 0, ParameterSetName = FromMemberParameterSetName)] + [ValidateTrustedData] public String Name { get; set; } /// @@ -137,6 +139,7 @@ public String[] MemberDefinition /// The path to the source code or DLL to load. /// [Parameter(Mandatory = true, Position = 0, ParameterSetName = FromPathParameterSetName)] + [ValidateTrustedData] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public string[] Path { @@ -183,6 +186,7 @@ public string[] Path /// [Parameter(Mandatory = true, ParameterSetName = FromLiteralPathParameterSetName)] [Alias("PSPath", "LP")] + [ValidateTrustedData] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public string[] LiteralPath { @@ -269,6 +273,7 @@ private void ProcessPaths(List resolvedPaths) /// [Parameter(Mandatory = true, ParameterSetName = FromAssemblyNameParameterSetName)] [Alias("AN")] + [ValidateTrustedData] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public String[] AssemblyName { get; set; } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Import-LocalizedData.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Import-LocalizedData.cs index be81fe8e764..cc3fa79cb19 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Import-LocalizedData.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Import-LocalizedData.cs @@ -96,6 +96,7 @@ public string FileName /// The command allowed in the data file. If unspecified, then ConvertFrom-StringData is allowed. /// [Parameter] + [ValidateTrustedData] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Cmdlets use arrays for parameters.")] public string[] SupportedCommand { @@ -205,6 +206,13 @@ protected override void ProcessRecord() else { variable.Value = result; + + if (Context.LanguageMode == PSLanguageMode.ConstrainedLanguage) + { + // Mark untrusted values for assignments to 'Global:' variables, and 'Script:' variables in + // a module scope, if it's necessary. + ExecutionContext.MarkObjectAsUntrustedForVariableAssignment(variable, scope, Context.EngineSessionState); + } } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/InvokeCommandCmdlet.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/InvokeCommandCmdlet.cs index fba4095da48..f9f44405382 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/InvokeCommandCmdlet.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/InvokeCommandCmdlet.cs @@ -20,6 +20,7 @@ public sealed /// Command to execute. /// [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)] + [ValidateTrustedData] public string Command { get; set; } #endregion parameters diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Var.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Var.cs index 0397570c6d5..b6527bac04c 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Var.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Var.cs @@ -941,6 +941,16 @@ private void SetVariable(string[] varNames, object varValue) if (varValue != AutomationNull.Value) { matchingVariable.Value = varValue; + + if (Context.LanguageMode == PSLanguageMode.ConstrainedLanguage) + { + // In 'ConstrainedLanguage' we want to monitor untrusted values assigned to 'Global:' variables + // and 'Script:' variables, because they may be set from 'ConstrainedLanguage' environment and + // referenced within trusted script block, and thus result in security issues. + // Here we are setting the value of an existing variable and don't know what scope this variable + // is from, so we mark the value as untrusted, regardless of the scope. + ExecutionContext.MarkObjectAsUntrusted(matchingVariable.Value); + } } if (Description != null) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/new-object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/new-object.cs index 99fc94e0dc8..06bd984898d 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/new-object.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/new-object.cs @@ -28,6 +28,7 @@ public sealed class NewObjectCommand : PSCmdlet /// the number [Parameter(ParameterSetName = netSetName, Mandatory = true, Position = 0)] + [ValidateTrustedData] public string TypeName { get; set; } = null; #if !UNIX @@ -36,6 +37,7 @@ public sealed class NewObjectCommand : PSCmdlet /// The ProgID of the Com object. /// [Parameter(ParameterSetName = "Com", Mandatory = true, Position = 0)] + [ValidateTrustedData] public string ComObject { get; set; } = null; #endif @@ -44,6 +46,7 @@ public sealed class NewObjectCommand : PSCmdlet /// /// [Parameter(ParameterSetName = netSetName, Mandatory = false, Position = 1)] + [ValidateTrustedData] [Alias("Args")] public object[] ArgumentList { get; set; } = null; @@ -58,6 +61,7 @@ public sealed class NewObjectCommand : PSCmdlet /// gets the properties to be set. /// [Parameter] + [ValidateTrustedData] [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public IDictionary Property { get; set; } diff --git a/src/System.Management.Automation/engine/ArgumentTypeConverterAttribute.cs b/src/System.Management.Automation/engine/ArgumentTypeConverterAttribute.cs index 66714772184..dda30c4b725 100644 --- a/src/System.Management.Automation/engine/ArgumentTypeConverterAttribute.cs +++ b/src/System.Management.Automation/engine/ArgumentTypeConverterAttribute.cs @@ -165,6 +165,13 @@ internal object Transform(EngineIntrinsics engineIntrinsics, object inputData, b throw new ArgumentTransformationMetadataException(e.Message, e); } + // Track the flow of untrusted object during the conversion when it's called directly from ParameterBinderBase. + // When it's called from the override Transform method, the tracking is taken care of in the base type. + if (bindingParameters || bindingScriptCmdlet) + { + ExecutionContext.PropagateInputSource(inputData, result, engineIntrinsics.SessionState.Internal.LanguageMode); + } + return result; } diff --git a/src/System.Management.Automation/engine/Attributes.cs b/src/System.Management.Automation/engine/Attributes.cs index 8095634bd03..5496b51bbc4 100644 --- a/src/System.Management.Automation/engine/Attributes.cs +++ b/src/System.Management.Automation/engine/Attributes.cs @@ -1745,6 +1745,36 @@ public interface IValidateSetValuesGenerator string[] GetValidValues(); } + /// + /// Validates that each parameter argument is Trusted data + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class ValidateTrustedDataAttribute : ValidateArgumentsAttribute + { + /// + /// Validates that the parameter argument is not untrusted + /// + /// Object to validate + /// + /// The engine APIs for the context under which the validation is being + /// evaluated. + /// + /// + /// if the argument is untrusted. + /// + protected override void Validate(object arguments, EngineIntrinsics engineIntrinsics) + { + if (ExecutionContext.HasEverUsedConstrainedLanguage && + engineIntrinsics.SessionState.Internal.ExecutionContext.LanguageMode == PSLanguageMode.FullLanguage) + { + if (ExecutionContext.IsMarkedAsUntrusted(arguments)) + { + throw new ValidationMetadataException("ValidateTrustedDataFailure", null, Metadata.ValidateTrustedDataFailure, arguments); + } + } + } + } + #region Allow /// @@ -2151,6 +2181,27 @@ protected ArgumentTransformationAttribute() /// should be thrown for any problems during transformation public abstract object Transform(EngineIntrinsics engineIntrinsics, object inputData); + /// + /// Transform inputData and track the flow of untrusted object. + /// NOTE: All internal handling of ArgumentTransformationAttribute should use this method to track the trustworthiness of + /// the data input source by default. + /// + /// + /// The default value for is True. + /// You should stick to the default value for this parameter in most cases so that data input source is tracked during the transformation. + /// The only acceptable exception is when this method is used in Compiler or Binder where you can generate extra code to track input source + /// when it's necessary. This is to minimize the overhead when tracking is not needed. + /// + internal object TransformInternal(EngineIntrinsics engineIntrinsics, object inputData, bool trackDataInputSource = true) + { + object result = Transform(engineIntrinsics, inputData); + if (trackDataInputSource && engineIntrinsics != null) + { + ExecutionContext.PropagateInputSource(inputData, result, engineIntrinsics.SessionState.Internal.LanguageMode); + } + return result; + } + /// /// The property is only checked when: /// a) The parameter is not mandatory diff --git a/src/System.Management.Automation/engine/CmdletInfo.cs b/src/System.Management.Automation/engine/CmdletInfo.cs index 0926911f752..cae7d0b9f58 100644 --- a/src/System.Management.Automation/engine/CmdletInfo.cs +++ b/src/System.Management.Automation/engine/CmdletInfo.cs @@ -61,6 +61,9 @@ internal CmdletInfo( _helpFilePath = helpFile; _PSSnapin = PSSnapin; _options = ScopedItemOptions.ReadOnly; + + // CmdletInfo represents cmdlets exposed from assemblies. On a locked down system, only trusted + // assemblies will be loaded. Therefore, a CmdletInfo instance will always be trusted. this.DefiningLanguageMode = PSLanguageMode.FullLanguage; } diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index b2e882d3ed6..af078216548 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -308,7 +308,7 @@ internal List GetResults(PowerShell powerShell, out int replac try { // Tab expansion is called from a trusted function - we should apply ConstrainedLanguage if necessary. - if (ExecutionContext.HasEverUsedConstrainedLanguage) + if (completionContext.ExecutionContext.HasRunspaceEverUsedConstrainedLanguageMode) { previousLanguageMode = completionContext.ExecutionContext.LanguageMode; completionContext.ExecutionContext.LanguageMode = PSLanguageMode.ConstrainedLanguage; diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 595e069a1fc..2c393e2a5a9 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -6741,7 +6741,7 @@ internal static bool TrySafeEval(ExpressionAst ast, ExecutionContext executionCo try { // ConstrainedLanguage has already been applied as necessary when we construct CompletionContext - Diagnostics.Assert(!(ExecutionContext.HasEverUsedConstrainedLanguage && executionContext.LanguageMode != PSLanguageMode.ConstrainedLanguage), + Diagnostics.Assert(!(executionContext.HasRunspaceEverUsedConstrainedLanguageMode && executionContext.LanguageMode != PSLanguageMode.ConstrainedLanguage), "If the runspace has ever used constrained language mode, then the current language mode should already be set to constrained language"); // We're passing 'true' here for isTrustedInput, because SafeExprEvaluator ensures that the AST diff --git a/src/System.Management.Automation/engine/CommandCompletion/PseudoParameterBinder.cs b/src/System.Management.Automation/engine/CommandCompletion/PseudoParameterBinder.cs index 1365d11999e..7feacdd8376 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/PseudoParameterBinder.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/PseudoParameterBinder.cs @@ -963,7 +963,7 @@ internal PseudoBindingInfo DoPseudoParameterBinding(CommandAst command, Type pip try { // Tab expansion is called from a trusted function - we should apply ConstrainedLanguage if necessary. - if (ExecutionContext.HasEverUsedConstrainedLanguage) + if (executionContext.HasRunspaceEverUsedConstrainedLanguageMode) { previousLanguageMode = executionContext.LanguageMode; executionContext.LanguageMode = PSLanguageMode.ConstrainedLanguage; diff --git a/src/System.Management.Automation/engine/CommandProcessor.cs b/src/System.Management.Automation/engine/CommandProcessor.cs index fff572e4290..1404cf5ea9f 100644 --- a/src/System.Management.Automation/engine/CommandProcessor.cs +++ b/src/System.Management.Automation/engine/CommandProcessor.cs @@ -208,7 +208,44 @@ internal override void Prepare(IDictionary psDefaultParameterValues) this.Command != null, "CommandProcessor did not initialize Command\n" + this.CommandInfo.Name); - BindCommandLineParameters(); + PSLanguageMode? oldLanguageMode = null; + bool? oldLangModeTransitionStatus = null; + try + { + var scriptCmdletInfo = this.CommandInfo as IScriptCommandInfo; + if (scriptCmdletInfo != null && + scriptCmdletInfo.ScriptBlock.LanguageMode.HasValue && + scriptCmdletInfo.ScriptBlock.LanguageMode != Context.LanguageMode) + { + // Set the language mode before parameter binding if it's necessary for a script cmdlet, so that the language + // mode is appropriately applied for evaluating parameter defaults and argument type conversion. + oldLanguageMode = Context.LanguageMode; + Context.LanguageMode = scriptCmdletInfo.ScriptBlock.LanguageMode.Value; + + // If it's from ConstrainedLanguage to FullLanguage, indicate the transition before parameter binding takes place. + if (oldLanguageMode == PSLanguageMode.ConstrainedLanguage && Context.LanguageMode == PSLanguageMode.FullLanguage) + { + oldLangModeTransitionStatus = Context.LanguageModeTransitionInParameterBinding; + Context.LanguageModeTransitionInParameterBinding = true; + } + } + + BindCommandLineParameters(); + } + finally + { + if (oldLanguageMode.HasValue) + { + // Revert to the original language mode after doing the parameter binding + Context.LanguageMode = oldLanguageMode.Value; + } + + if (oldLangModeTransitionStatus.HasValue) + { + // Revert the transition state to old value after doing the parameter binding + Context.LanguageModeTransitionInParameterBinding = oldLangModeTransitionStatus.Value; + } + } } protected override void OnSetCurrentScope() diff --git a/src/System.Management.Automation/engine/ExecutionContext.cs b/src/System.Management.Automation/engine/ExecutionContext.cs index 4ebc0d2bb52..f4b749b7dac 100644 --- a/src/System.Management.Automation/engine/ExecutionContext.cs +++ b/src/System.Management.Automation/engine/ExecutionContext.cs @@ -8,6 +8,7 @@ using System.Management.Automation.Host; using System.Management.Automation.Internal; using System.Management.Automation.Internal.Host; +using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using System.Runtime.CompilerServices; using Microsoft.PowerShell; @@ -314,16 +315,38 @@ internal PSLanguageMode LanguageMode // caches. After that, the binding rules encode the language mode. if (value == PSLanguageMode.ConstrainedLanguage) { - ExecutionContext.HasEverUsedConstrainedLanguage = true; HasRunspaceEverUsedConstrainedLanguageMode = true; - System.Management.Automation.Language.PSSetMemberBinder.InvalidateCache(); - System.Management.Automation.Language.PSInvokeMemberBinder.InvalidateCache(); - System.Management.Automation.Language.PSConvertBinder.InvalidateCache(); - System.Management.Automation.Language.PSBinaryOperationBinder.InvalidateCache(); - System.Management.Automation.Language.PSGetIndexBinder.InvalidateCache(); - System.Management.Automation.Language.PSSetIndexBinder.InvalidateCache(); - System.Management.Automation.Language.PSCreateInstanceBinder.InvalidateCache(); + // If 'ExecutionContext.HasEverUsedConstrainedLanguage' is already set to True, then we have + // already invalidated all cached binders, and binders already started to generate code with + // consideration of 'LanguageMode'. In such case, we don't need to invalidate cached binders + // again. + // Note that when executing script blocks marked as 'FullLanguage' in a 'ConstrainedLanguage' + // environment, we will set and Restore 'context.LanguageMode' very often. But we should not + // invalidate the cached binders every time we restore to 'ConstrainedLanguage'. + if (!ExecutionContext.HasEverUsedConstrainedLanguage) + { + lock (lockObject) + { + // If another thread has already set 'ExecutionContext.HasEverUsedConstrainedLanguage' + // while we are waiting on the lock, then nothing needs to be done. + if (!ExecutionContext.HasEverUsedConstrainedLanguage) + { + PSSetMemberBinder.InvalidateCache(); + PSInvokeMemberBinder.InvalidateCache(); + PSConvertBinder.InvalidateCache(); + PSBinaryOperationBinder.InvalidateCache(); + PSGetIndexBinder.InvalidateCache(); + PSSetIndexBinder.InvalidateCache(); + PSCreateInstanceBinder.InvalidateCache(); + + // Set 'HasEverUsedConstrainedLanguage' at the very end to guarantee other threads to wait until + // all invalidation operations are done. + UntrustedObjects = new ConditionalWeakTable(); + ExecutionContext.HasEverUsedConstrainedLanguage = true; + } + } + } } // Conversion caches don't have version info / binding rules, so must be @@ -340,12 +363,106 @@ internal PSLanguageMode LanguageMode /// internal bool HasRunspaceEverUsedConstrainedLanguageMode { get; private set; } + /// + /// Indicate if a parameter binding is happening that transitions the execution from ConstrainedLanguage + /// mode to a trusted FullLanguage command. + /// + internal bool LanguageModeTransitionInParameterBinding { get; set; } + /// /// True if we've ever used ConstrainedLanguage. If this is the case, then the binding restrictions /// need to also validate against the language mode. /// internal static bool HasEverUsedConstrainedLanguage { get; private set; } + #region Variable Tracking + + /// + /// Initialized when 'ConstrainedLanguage' is applied. + /// The objects contained in this table are considered to be untrusted. + /// + private static ConditionalWeakTable UntrustedObjects { get; set; } + + /// + /// Helper for checking if the given value is marked as untrusted. + /// + internal static bool IsMarkedAsUntrusted(object value) + { + bool result = false; + var baseValue = PSObject.Base(value); + if (baseValue != null && baseValue != NullString.Value) + { + object unused; + result = UntrustedObjects.TryGetValue(baseValue, out unused); + } + return result; + } + + /// + /// Helper for marking a value as untrusted. + /// + internal static void MarkObjectAsUntrusted(object value) + { + // If the value is a PSObject, then we mark its base object untrusted + var baseValue = PSObject.Base(value); + if (baseValue != null && baseValue != NullString.Value) + { + // It's actually setting a key value pair when the key doesn't exist + UntrustedObjects.GetValue(baseValue, key => null); + + try + { + // If it's a PSReference object, we need to also mark the value it's holding on. + // This could result in a recursion if psRef.Value points to itself directly or indirectly, so we check if psRef.Value is already + // marked before making a recursive call. The additional check adds extra overhead for handling PSReference object, but it should + // be rare in practice. + var psRef = baseValue as PSReference; + if (psRef != null && !IsMarkedAsUntrusted(psRef.Value)) + { + MarkObjectAsUntrusted(psRef.Value); + } + } + catch { /* psRef.Value may call PSVariable.Value under the hood, which may throw arbitrary exception */ } + } + } + + /// + /// Helper for setting the untrusted value of an assignment to either a 'Global:' variable, or a 'Script:' variable in a module scope. + /// + /// + /// This method is for tracking assignment to global variables and module script scope varaibles in ConstrainedLanguage mode. Those variables + /// can go across boundaries between ConstrainedLanguage and FullLanguage, and make it easy for a trusted script to use data from an untrusted + /// environment. Therefore, in ConstrainedLanguage mode, we need to mark the value objects assigned to those variables as untrusted. + /// + internal static void MarkObjectAsUntrustedForVariableAssignment(PSVariable variable, SessionStateScope scope, SessionStateInternal sessionState) + { + if (scope.Parent == null || // If it's the global scope, OR + (sessionState.Module != null && // it's running in a module AND + scope.ScriptScope == scope && scope.Parent.Parent == null)) // it's the module's script scope (scope.Parent is global scope and scope.ScriptScope points to itself) + { + // We are setting value for either a 'Global:' variable, or a 'Script:' variable within a module in 'ConstrainedLanguage' mode. + // Global variable may be referenced within trusted script block (scriptBlock.LanguageMode == 'FullLanguage'), and users could + // also set a 'Script:' variable in a trusted module scope from 'ConstrainedLanguage' environment via '& $mo { $script: }'. + // So we need to mark the value as untrusted. + MarkObjectAsUntrusted(variable.Value); + } + } + + /// + /// The result object is assumed generated by operating on the original object. + /// So if the original object is from an untrusted input source, we mark the result object as untrusted. + /// + internal static void PropagateInputSource(object originalObject, object resultObject, PSLanguageMode currentLanguageMode) + { + // The untrusted flag is populated only in FullLanguage mode and ConstrainedLanguage has been used in the process before. + if (ExecutionContext.HasEverUsedConstrainedLanguage && currentLanguageMode == PSLanguageMode.FullLanguage && IsMarkedAsUntrusted(originalObject)) + { + MarkObjectAsUntrusted(resultObject); + } + } + + #endregion + /// /// If true the PowerShell debugger will use FullLanguage mode, otherwise it will use the current language mode /// @@ -1469,9 +1586,10 @@ private void InitializeCommon(AutomationEngine engine, PSHost hostInterface) Modules = new ModuleIntrinsics(this); } + private static object lockObject = new Object(); + #if !CORECLR // System.AppDomain is not in CoreCLR private static bool _assemblyEventHandlerSet = false; - private static object lockObject = new Object(); /// /// AssemblyResolve event handler that will look in the assembly cache to see diff --git a/src/System.Management.Automation/engine/InternalCommands.cs b/src/System.Management.Automation/engine/InternalCommands.cs index e62555bd03b..63252ec2d04 100644 --- a/src/System.Management.Automation/engine/InternalCommands.cs +++ b/src/System.Management.Automation/engine/InternalCommands.cs @@ -162,6 +162,7 @@ public ScriptBlock[] RemainingScripts /// The property or method name /// [Parameter(Mandatory = true, Position = 0, ParameterSetName = "PropertyAndMethodSet")] + [ValidateTrustedData] [ValidateNotNullOrEmpty] public string MemberName { @@ -177,6 +178,7 @@ public string MemberName /// [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Cmdlets use arrays for parameters.")] [Parameter(ParameterSetName = "PropertyAndMethodSet", ValueFromRemainingArguments = true)] + [ValidateTrustedData] [Alias("Args")] public object[] ArgumentList { diff --git a/src/System.Management.Automation/engine/LanguagePrimitives.cs b/src/System.Management.Automation/engine/LanguagePrimitives.cs index 13ecf6e1291..8f2398d93a8 100644 --- a/src/System.Management.Automation/engine/LanguagePrimitives.cs +++ b/src/System.Management.Automation/engine/LanguagePrimitives.cs @@ -3851,7 +3851,12 @@ internal object Convert(object valueToConvert, ExecutionContext ecFromTLS = LocalPipeline.GetExecutionContextFromTLS(); object result = null; - if (ecFromTLS == null || ecFromTLS.LanguageMode == PSLanguageMode.FullLanguage) + // Setting arbitrary properties is dangerous, so we allow this only if + // - It's running on a thread without Runspace; Or + // - It's in FullLanguage but not because it's part of a parameter binding that is transitioning from ConstrainedLanguage to FullLanguage + // When this is invoked from a parameter binding in transition from ConstrainedLanguage environment to FullLanguage command, we disallow + // the property conversion because it's dangerous. + if (ecFromTLS == null || (ecFromTLS.LanguageMode == PSLanguageMode.FullLanguage && !ecFromTLS.LanguageModeTransitionInParameterBinding)) { result = _constructor(); var psobject = valueToConvert as PSObject; diff --git a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs index c65bf5c53e5..96e19952cba 100644 --- a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs @@ -77,6 +77,7 @@ public string Prefix [Parameter(ParameterSetName = ParameterSet_Name, Mandatory = true, ValueFromPipeline = true, Position = 0)] [Parameter(ParameterSetName = ParameterSet_ViaPsrpSession, Mandatory = true, ValueFromPipeline = true, Position = 0)] [Parameter(ParameterSetName = ParameterSet_ViaCimSession, Mandatory = true, ValueFromPipeline = true, Position = 0)] + [ValidateTrustedData] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Cmdlets use arrays for parameters.")] public string[] Name { set; get; } = Utils.EmptyArray(); @@ -85,6 +86,7 @@ public string Prefix /// [Parameter(ParameterSetName = ParameterSet_FQName, Mandatory = true, ValueFromPipeline = true, Position = 0)] [Parameter(ParameterSetName = ParameterSet_FQName_ViaPsrpSession, Mandatory = true, ValueFromPipeline = true, Position = 0)] + [ValidateTrustedData] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Cmdlets use arrays for parameters.")] public ModuleSpecification[] FullyQualifiedName { get; set; } @@ -93,6 +95,7 @@ public string Prefix /// [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Cmdlets use arrays for parameters.")] [Parameter(ParameterSetName = ParameterSet_Assembly, Mandatory = true, ValueFromPipeline = true, Position = 0)] + [ValidateTrustedData] public Assembly[] Assembly { get; set; } /// @@ -294,6 +297,7 @@ public Version RequiredVersion /// This parameter specifies the current pipeline object /// [Parameter(ParameterSetName = ParameterSet_ModuleInfo, Mandatory = true, ValueFromPipeline = true, Position = 0)] + [ValidateTrustedData] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Cmdlets use arrays for parameters.")] public PSModuleInfo[] ModuleInfo { set; get; } = Utils.EmptyArray(); diff --git a/src/System.Management.Automation/engine/ParameterBinderBase.cs b/src/System.Management.Automation/engine/ParameterBinderBase.cs index d3f546e558e..5601b3340fe 100644 --- a/src/System.Management.Automation/engine/ParameterBinderBase.cs +++ b/src/System.Management.Automation/engine/ParameterBinderBase.cs @@ -414,7 +414,7 @@ internal virtual bool BindParameter( parameterMetadata.CannotBeNull || dma.TransformNullOptionalParameters))) { - parameterValue = dma.Transform(_engine, parameterValue); + parameterValue = dma.TransformInternal(_engine, parameterValue); } } @@ -991,6 +991,7 @@ private object CoerceTypeAsNeeded( collectionTypeInfo = new ParameterCollectionTypeInformation(toType); } + object originalValue = currentValue; object result = currentValue; using (bindingTracer.TraceScope( @@ -1225,29 +1226,23 @@ private object CoerceTypeAsNeeded( bindingTracer.WriteLine( "CONVERT arg type to param type using LanguagePrimitives.ConvertTo"); - // If we are in constrained language mode and the target command is trusted, - // allow type conversion to the target command's parameter type. - // Don't allow Hashtable-to-Object conversion (PSObject and IDictionary), though, - // as those can lead to property setters that probably aren't expected. - bool changeLanguageModeForTrustedCommand = false; - if (_context.LanguageMode == PSLanguageMode.ConstrainedLanguage) - { - var basedObject = PSObject.Base(currentValue); - var supportsPropertyConversion = basedObject is PSObject; - var supportsIDictionaryConversion = (basedObject != null) && - (typeof(IDictionary).IsAssignableFrom(basedObject.GetType())); - - changeLanguageModeForTrustedCommand = - (this.Command.CommandInfo.DefiningLanguageMode == PSLanguageMode.FullLanguage) && - (!supportsPropertyConversion) && - (!supportsIDictionaryConversion); - } + // If we are in constrained language mode and the target command is trusted, which is often + // the case for C# cmdlets, then we allow type conversion to the target parameter type. + // + // However, we don't allow Hashtable-to-Object conversion (PSObject and IDictionary) because + // those can lead to property setters that probably aren't expected. This is enforced by + // setting 'Context.LanguageModeTransitionInParameterBinding' to true before the conversion. + bool changeLanguageModeForTrustedCommand = + Context.LanguageMode == PSLanguageMode.ConstrainedLanguage && + this.Command.CommandInfo.DefiningLanguageMode == PSLanguageMode.FullLanguage; + bool oldLangModeTransitionStatus = Context.LanguageModeTransitionInParameterBinding; try { if (changeLanguageModeForTrustedCommand) { - _context.LanguageMode = PSLanguageMode.FullLanguage; + Context.LanguageMode = PSLanguageMode.FullLanguage; + Context.LanguageModeTransitionInParameterBinding = true; } result = LanguagePrimitives.ConvertTo(currentValue, toType, CultureInfo.CurrentCulture); @@ -1256,7 +1251,8 @@ private object CoerceTypeAsNeeded( { if (changeLanguageModeForTrustedCommand) { - _context.LanguageMode = PSLanguageMode.ConstrainedLanguage; + Context.LanguageMode = PSLanguageMode.ConstrainedLanguage; + Context.LanguageModeTransitionInParameterBinding = oldLangModeTransitionStatus; } } @@ -1311,6 +1307,13 @@ private object CoerceTypeAsNeeded( throw pbe; } } // TraceScope + + if (result != null) + { + // Set the converted result object untrusted if necessary + ExecutionContext.PropagateInputSource(originalValue, result, Context.LanguageMode); + } + return result; } // CoerceTypeAsNeeded @@ -1444,6 +1447,7 @@ private object EncodeCollection( bool coerceElementTypeIfNeeded, out bool coercionRequired) { + object originalValue = currentValue; object result = null; coercionRequired = false; @@ -1870,6 +1874,9 @@ private object EncodeCollection( if (!coercionRequired) { result = resultCollection; + + // Set the converted result object untrusted if necessary + ExecutionContext.PropagateInputSource(originalValue, result, Context.LanguageMode); } } while (false); diff --git a/src/System.Management.Automation/engine/ScriptCommandProcessor.cs b/src/System.Management.Automation/engine/ScriptCommandProcessor.cs index d2a186edb2e..dae14fe3d4a 100644 --- a/src/System.Management.Automation/engine/ScriptCommandProcessor.cs +++ b/src/System.Management.Automation/engine/ScriptCommandProcessor.cs @@ -476,7 +476,26 @@ private void RunClause(Action clause, object dollarUnderbar, ob Context.LanguageMode = newLanguageMode.Value; } - EnterScope(); + bool? oldLangModeTransitionStatus = null; + try + { + // If it's from ConstrainedLanguage to FullLanguage, indicate the transition before parameter binding takes place. + if (oldLanguageMode == PSLanguageMode.ConstrainedLanguage && newLanguageMode == PSLanguageMode.FullLanguage) + { + oldLangModeTransitionStatus = Context.LanguageModeTransitionInParameterBinding; + Context.LanguageModeTransitionInParameterBinding = true; + } + + EnterScope(); + } + finally + { + if (oldLangModeTransitionStatus.HasValue) + { + // Revert the transition state to old value after doing the parameter binding + Context.LanguageModeTransitionInParameterBinding = oldLangModeTransitionStatus.Value; + } + } if (commandRuntime.ErrorMergeTo == MshCommandRuntime.MergeDataStream.Output) { diff --git a/src/System.Management.Automation/engine/SessionStateScope.cs b/src/System.Management.Automation/engine/SessionStateScope.cs index 72a0b8dc90b..98b7e2aeb59 100644 --- a/src/System.Management.Automation/engine/SessionStateScope.cs +++ b/src/System.Management.Automation/engine/SessionStateScope.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation.Internal; +using System.Management.Automation.Runspaces; namespace System.Management.Automation { @@ -484,19 +485,9 @@ internal PSVariable SetVariable(string name, object value, bool asValue, bool fo variable = (LocalsTuple != null ? LocalsTuple.TrySetVariable(name, value) : null) ?? new PSVariable(name, value); } - // Don't let people set AllScope variables in ConstrainedLanguage, - // as they can be used to interfere with the session state of - // trusted commands. if (ExecutionContext.HasEverUsedConstrainedLanguage) { - var context = System.Management.Automation.Runspaces.LocalPipeline.GetExecutionContextFromTLS(); - - if ((context != null) && - (context.LanguageMode == PSLanguageMode.ConstrainedLanguage) && - ((variable.Options & ScopedItemOptions.AllScope) == ScopedItemOptions.AllScope)) - { - throw new PSNotSupportedException(); - } + CheckVariableChangeInConstrainedLanguage(variable); } _variables[name] = variable; @@ -592,19 +583,9 @@ internal PSVariable NewVariable(PSVariable newVariable, bool force, SessionState variable = newVariable; } - // Don't let people set AllScope variables in ConstrainedLanguage, - // as they can be used to interfere with the session state of - // trusted commands. if (ExecutionContext.HasEverUsedConstrainedLanguage) { - var context = System.Management.Automation.Runspaces.LocalPipeline.GetExecutionContextFromTLS(); - - if ((context != null) && - (context.LanguageMode == PSLanguageMode.ConstrainedLanguage) && - ((variable.Options & ScopedItemOptions.AllScope) == ScopedItemOptions.AllScope)) - { - throw new PSNotSupportedException(); - } + CheckVariableChangeInConstrainedLanguage(variable); } _variables[variable.Name] = variable; @@ -1978,6 +1959,24 @@ private void RemoveAliasFromCache(string alias, string value) } } + private void CheckVariableChangeInConstrainedLanguage(PSVariable variable) + { + var context = LocalPipeline.GetExecutionContextFromTLS(); + if (context?.LanguageMode == PSLanguageMode.ConstrainedLanguage) + { + if ((variable.Options & ScopedItemOptions.AllScope) == ScopedItemOptions.AllScope) + { + // Don't let people set AllScope variables in ConstrainedLanguage, as they can be used to + // interfere with the session state of trusted commands. + throw new PSNotSupportedException(); + } + + // Mark untrusted values for assignments to 'Global:' variables, and 'Script:' variables in + // a module scope, if it's necessary. + ExecutionContext.MarkObjectAsUntrustedForVariableAssignment(variable, this, context.EngineSessionState); + } + } + #endregion } // class SessionStateScope } // namespace System.Management.Automation diff --git a/src/System.Management.Automation/engine/ShellVariable.cs b/src/System.Management.Automation/engine/ShellVariable.cs index 668685eab97..8865391ee94 100644 --- a/src/System.Management.Automation/engine/ShellVariable.cs +++ b/src/System.Management.Automation/engine/ShellVariable.cs @@ -499,7 +499,7 @@ internal static object TransformValue(IEnumerable attributes, object attribute as ArgumentTransformationAttribute; if (transformationAttribute != null) { - result = transformationAttribute.Transform(engine, result); + result = transformationAttribute.TransformInternal(engine, result); } } return result; diff --git a/src/System.Management.Automation/engine/VariableAttributeCollection.cs b/src/System.Management.Automation/engine/VariableAttributeCollection.cs index b7a88cff51d..8bcc7d6985a 100644 --- a/src/System.Management.Automation/engine/VariableAttributeCollection.cs +++ b/src/System.Management.Automation/engine/VariableAttributeCollection.cs @@ -131,7 +131,7 @@ private object VerifyNewAttribute(Attribute item) engine = context.EngineIntrinsics; } - variableValue = argumentTransformation.Transform(engine, variableValue); + variableValue = argumentTransformation.TransformInternal(engine, variableValue); } if (!PSVariable.IsValidValue(variableValue, item)) diff --git a/src/System.Management.Automation/engine/parser/TypeResolver.cs b/src/System.Management.Automation/engine/parser/TypeResolver.cs index 3128d30162c..9bc5854710e 100644 --- a/src/System.Management.Automation/engine/parser/TypeResolver.cs +++ b/src/System.Management.Automation/engine/parser/TypeResolver.cs @@ -783,6 +783,7 @@ internal static class CoreTypes { typeof(ValidateRangeAttribute), new[] { "ValidateRange" } }, { typeof(ValidateScriptAttribute), new[] { "ValidateScript" } }, { typeof(ValidateSetAttribute), new[] { "ValidateSet" } }, + { typeof(ValidateTrustedDataAttribute), new[] { "ValidateTrustedData" } }, { typeof(ValidateUserDriveAttribute), new[] { "ValidateUserDrive"} }, { typeof(Version), new[] { "version" } }, { typeof(void), new[] { "void" } }, diff --git a/src/System.Management.Automation/engine/remoting/commands/StartJob.cs b/src/System.Management.Automation/engine/remoting/commands/StartJob.cs index 2246f42f6b0..930cea82a64 100644 --- a/src/System.Management.Automation/engine/remoting/commands/StartJob.cs +++ b/src/System.Management.Automation/engine/remoting/commands/StartJob.cs @@ -35,6 +35,7 @@ public class StartJobCommand : PSExecutionCmdlet, IDisposable /// [Parameter(Position = 0, Mandatory = true, ParameterSetName = StartJobCommand.DefinitionNameParameterSet)] + [ValidateTrustedData] [ValidateNotNullOrEmpty] public string DefinitionName { @@ -106,6 +107,7 @@ public virtual String Name [Parameter(Position = 0, Mandatory = true, ParameterSetName = StartJobCommand.ComputerNameParameterSet)] + [ValidateTrustedData] [Alias("Command")] public override ScriptBlock ScriptBlock { @@ -284,6 +286,7 @@ public override Uri[] ConnectionUri Position = 0, Mandatory = true, ParameterSetName = StartJobCommand.FilePathComputerNameParameterSet)] + [ValidateTrustedData] public override string FilePath { get @@ -302,6 +305,7 @@ public override string FilePath [Parameter( Mandatory = true, ParameterSetName = StartJobCommand.LiteralFilePathComputerNameParameterSet)] + [ValidateTrustedData] [Alias("PSPath","LP")] public string LiteralPath { @@ -433,6 +437,7 @@ public override PSSessionOption SessionOption ParameterSetName = StartJobCommand.ComputerNameParameterSet)] [Parameter(Position = 1, ParameterSetName = StartJobCommand.LiteralFilePathComputerNameParameterSet)] + [ValidateTrustedData] public virtual ScriptBlock InitializationScript { get { return _initScript; } @@ -485,6 +490,7 @@ public virtual Version PSVersion ParameterSetName = StartJobCommand.ComputerNameParameterSet)] [Parameter(ValueFromPipeline = true, ParameterSetName = StartJobCommand.LiteralFilePathComputerNameParameterSet)] + [ValidateTrustedData] public override PSObject InputObject { get { return base.InputObject; } @@ -497,6 +503,7 @@ public override PSObject InputObject [Parameter(ParameterSetName = StartJobCommand.FilePathComputerNameParameterSet)] [Parameter(ParameterSetName = StartJobCommand.ComputerNameParameterSet)] [Parameter(ParameterSetName = StartJobCommand.LiteralFilePathComputerNameParameterSet)] + [ValidateTrustedData] [Alias("Args")] [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public override Object[] ArgumentList diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs index 1839eab9c12..72f945a9efa 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs @@ -303,6 +303,15 @@ private static CommandProcessorBase AddCommand(PipelineProcessor pipe, internal static IEnumerable Splat(object splattedValue, Ast splatAst) { splattedValue = PSObject.Base(splattedValue); + + var markUntrustedData = false; + if (ExecutionContext.HasEverUsedConstrainedLanguage) + { + // If the value to be splatted is untrusted, then make sure sub-values held by it are + // also marked as untrusted. + markUntrustedData = ExecutionContext.IsMarkedAsUntrusted(splattedValue); + } + IDictionary splattedTable = splattedValue as IDictionary; if (splattedTable != null) { @@ -312,6 +321,7 @@ internal static IEnumerable Splat(object splattedValue object parameterValue = de.Value; string parameterText = GetParameterText(parameterName); + if (markUntrustedData) { ExecutionContext.MarkObjectAsUntrusted(parameterValue); } yield return CommandParameterInternal.CreateParameterWithArgument( splatAst, parameterName, parameterText, splatAst, parameterValue, false); @@ -324,6 +334,7 @@ internal static IEnumerable Splat(object splattedValue { foreach (object obj in enumerableValue) { + if (markUntrustedData) { ExecutionContext.MarkObjectAsUntrusted(obj); } yield return SplatEnumerableElement(obj, splatAst); } } diff --git a/src/System.Management.Automation/engine/runtime/Operations/VariableOps.cs b/src/System.Management.Automation/engine/runtime/Operations/VariableOps.cs index d1d1f71399a..5167156a13d 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/VariableOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/VariableOps.cs @@ -97,6 +97,13 @@ internal static object SetVariableValue(VariablePath variablePath, object value, var.Value = value; } + if (executionContext.LanguageMode == PSLanguageMode.ConstrainedLanguage) + { + // Mark untrusted values for assignments to 'Global:' variables, and 'Script:' variables in + // a module scope, if it's necessary. + ExecutionContext.MarkObjectAsUntrustedForVariableAssignment(var, scope, sessionState); + } + return value; } diff --git a/src/System.Management.Automation/resources/Metadata.resx b/src/System.Management.Automation/resources/Metadata.resx index f8a0d753788..61aad9f57b5 100644 --- a/src/System.Management.Automation/resources/Metadata.resx +++ b/src/System.Management.Automation/resources/Metadata.resx @@ -249,4 +249,7 @@ The Enum member '{0}' is not a valid value for the parameter '{1}'. Specify one of the following members and try again: {2}. + + Cannot process input. The argument "{0}" is not trusted. + diff --git a/test/powershell/Language/Parser/ParameterBinding.Tests.ps1 b/test/powershell/Language/Parser/ParameterBinding.Tests.ps1 index ad4d539d5cf..ff7ce0844f4 100644 --- a/test/powershell/Language/Parser/ParameterBinding.Tests.ps1 +++ b/test/powershell/Language/Parser/ParameterBinding.Tests.ps1 @@ -99,3 +99,329 @@ Describe 'Argument transformation attribute on optional argument with explicit $ Invoke-CSharpCmdletTakesUInt64 -Address $null | Should -Be 42 } } + +Describe "Custom type conversion in parameter binding" -Tags 'Feature' { + BeforeAll { + ## Prepare the script module + $content = @' + function Test-ScriptCmdlet { + [CmdletBinding(DefaultParameterSetName = "File")] + param( + [Parameter(Mandatory, ParameterSetName = "File")] + [System.IO.FileInfo] $File, + + [Parameter(Mandatory, ParameterSetName = "StartInfo")] + [System.Diagnostics.ProcessStartInfo] $StartInfo + ) + + if ($PSCmdlet.ParameterSetName -eq "File") { + $File.Name + } else { + $StartInfo.FileName + } + } + + function Test-ScriptFunction { + param( + [System.IO.FileInfo] $File, + [System.Diagnostics.ProcessStartInfo] $StartInfo + ) + + if ($null -ne $File) { + $File.Name + } + if ($null -ne $StartInfo) { + $StartInfo.FileName + } + } +'@ + Set-Content -Path $TestDrive\module.psm1 -Value $content -Force + + ## Prepare the C# module + $code = @' + using System.IO; + using System.Diagnostics; + using System.Management.Automation; + + namespace Test + { + [Cmdlet("Test", "BinaryCmdlet", DefaultParameterSetName = "File")] + public class TestCmdletCommand : PSCmdlet + { + [Parameter(Mandatory = true, ParameterSetName = "File")] + public FileInfo File { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = "StartInfo")] + public ProcessStartInfo StartInfo { get; set; } + + protected override void ProcessRecord() + { + if (this.ParameterSetName == "File") + { + WriteObject(File.Name); + } + else + { + WriteObject(StartInfo.FileName); + } + } + } + } +'@ + $asmFile = [System.IO.Path]::GetTempFileName() + ".dll" + Add-Type -TypeDefinition $code -OutputAssembly $asmFile + + ## Helper function to execute script + function Execute-Script { + [CmdletBinding(DefaultParameterSetName = "Script")] + param( + [Parameter(Mandatory)] + [powershell]$ps, + + [Parameter(Mandatory, ParameterSetName = "Script")] + [string]$Script, + + [Parameter(Mandatory, ParameterSetName = "Command")] + [string]$Command, + + [Parameter(Mandatory, ParameterSetName = "Command")] + [string]$ParameterName, + + [Parameter(Mandatory, ParameterSetName = "Command")] + [object]$Argument + ) + $ps.Commands.Clear() + $ps.Streams.ClearStreams() + + if ($PSCmdlet.ParameterSetName -eq "Script") { + $ps.AddScript($Script).Invoke() + } else { + $ps.AddCommand($Command).AddParameter($ParameterName, $Argument).Invoke() + } + } + + ## Helper command strings + $changeToConstrainedLanguage = '$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"' + $getLanguageMode = '$ExecutionContext.SessionState.LanguageMode' + $importScriptModule = "Import-Module $TestDrive\module.psm1" + $importCSharpModule = "Import-Module $asmFile" + } + + AfterAll { + ## Set the LanguageMode to force rebuilding the type conversion cache. + ## This is needed because type conversions happen in the new powershell runspace with 'ConstrainedLanguage' mode + ## will be put in the type conversion cache, and that may affect the default session. + $ExecutionContext.SessionState.LanguageMode = "FullLanguage" + } + + It "Custom type conversion in parameter binding is allowed in FullLanguage" { + ## Create a powershell instance for the test + $ps = [powershell]::Create() + try { + ## Import the modules in FullLanguage mode + Execute-Script -ps $ps -Script $importScriptModule + Execute-Script -ps $ps -Script $importCSharpModule + + $languageMode = Execute-Script -ps $ps -Script $getLanguageMode + $languageMode | Should Be 'FullLanguage' + + $result1 = Execute-Script -ps $ps -Script "Test-ScriptCmdlet -File fileToUse" + $result1 | Should Be "fileToUse" + + $result2 = Execute-Script -ps $ps -Script "Test-ScriptFunction -File fileToUse" + $result2 | Should Be "fileToUse" + + $result3 = Execute-Script -ps $ps -Script "Test-BinaryCmdlet -File fileToUse" + $result3 | Should Be "fileToUse" + + ## Conversion involves setting properties of an instance of the target type is allowed in FullLanguage mode + $hashValue = @{ FileName = "filename"; Arguments = "args" } + $psobjValue = [PSCustomObject] $hashValue + + ## Test 'Test-ScriptCmdlet -StartInfo' with IDictionary and PSObject with properties + $result4 = Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $hashValue + $result4 | Should Be "filename" + $result5 = Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $psobjValue + $result5 | Should Be "filename" + + ## Test 'Test-ScriptFunction -StartInfo' with IDictionary and PSObject with properties + $result6 = Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $hashValue + $result6 | Should Be "filename" + $result7 = Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $psobjValue + $result7 | Should Be "filename" + + ## Test 'Test-BinaryCmdlet -StartInfo' with IDictionary and PSObject with properties + $result8 = Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $hashValue + $result8 | Should Be "filename" + $result9 = Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $psobjValue + $result9 | Should Be "filename" + } + finally { + $ps.Dispose() + } + } + + It "Some custom type conversion in parameter binding is allowed for trusted cmdlets in ConstrainedLanguage" { + ## Create a powershell instance for the test + $ps = [powershell]::Create() + try { + ## Import the modules in FullLanguage mode + Execute-Script -ps $ps -Script $importScriptModule + Execute-Script -ps $ps -Script $importCSharpModule + + $languageMode = Execute-Script -ps $ps -Script $getLanguageMode + $languageMode | Should Be 'FullLanguage' + + ## Change to ConstrainedLanguage mode + Execute-Script -ps $ps -Script $changeToConstrainedLanguage + $languageMode = Execute-Script -ps $ps -Script $getLanguageMode + $languageMode | Should Be 'ConstrainedLanguage' + + $result1 = Execute-Script -ps $ps -Script "Test-ScriptCmdlet -File fileToUse" + $result1 | Should Be "fileToUse" + + $result2 = Execute-Script -ps $ps -Script "Test-ScriptFunction -File fileToUse" + $result2 | Should Be "fileToUse" + + $result3 = Execute-Script -ps $ps -Script "Test-BinaryCmdlet -File fileToUse" + $result3 | Should Be "fileToUse" + + ## If the conversion involves setting properties of an instance of the target type, + ## then it's disallowed even for trusted cmdlets. + $hashValue = @{ FileName = "filename"; Arguments = "args" } + $psobjValue = [PSCustomObject] $hashValue + + ## Test 'Test-ScriptCmdlet -StartInfo' with IDictionary and PSObject with properties + try { + Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $hashValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingArgumentTransformationException,Execute-Script" + } + + try { + Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $psobjValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingArgumentTransformationException,Execute-Script" + } + + ## Test 'Test-ScriptFunction -StartInfo' with IDictionary and PSObject with properties + try { + Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $hashValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingArgumentTransformationException,Execute-Script" + } + + try { + Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $psobjValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingArgumentTransformationException,Execute-Script" + } + + ## Test 'Test-BinaryCmdlet -StartInfo' with IDictionary and PSObject with properties + try { + Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $hashValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingException,Execute-Script" + } + + try { + Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $psobjValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingException,Execute-Script" + } + } + finally { + $ps.Dispose() + } + } + + It "Custom type conversion in parameter binding is NOT allowed for untrusted cmdlets in ConstrainedLanguage" { + ## Create a powershell instance for the test + $ps = [powershell]::Create() + try { + $languageMode = Execute-Script -ps $ps -Script $getLanguageMode + $languageMode | Should Be 'FullLanguage' + + ## Change to ConstrainedLanguage mode + Execute-Script -ps $ps -Script $changeToConstrainedLanguage + $languageMode = Execute-Script -ps $ps -Script $getLanguageMode + $languageMode | Should Be 'ConstrainedLanguage' + + ## Import the modules in ConstrainedLanguage mode + Execute-Script -ps $ps -Script $importScriptModule + Execute-Script -ps $ps -Script $importCSharpModule + + $result1 = Execute-Script -ps $ps -Script "Test-ScriptCmdlet -File fileToUse" + $result1 | Should Be $null + $ps.Streams.Error.Count | Should Be 1 + $ps.Streams.Error[0].FullyQualifiedErrorId | Should Be "ParameterArgumentTransformationError,Test-ScriptCmdlet" + + $result2 = Execute-Script -ps $ps -Script "Test-ScriptFunction -File fileToUse" + $result2 | Should Be $null + $ps.Streams.Error.Count | Should Be 1 + $ps.Streams.Error[0].FullyQualifiedErrorId | Should Be "ParameterArgumentTransformationError,Test-ScriptFunction" + + ## Binary cmdlets are always marked as trusted because only trusted assemblies can be loaded on DeviceGuard machine. + $result3 = Execute-Script -ps $ps -Script "Test-BinaryCmdlet -File fileToUse" + $result3 | Should Be "fileToUse" + + ## Conversion that involves setting properties of an instance of the target type is disallowed. + $hashValue = @{ FileName = "filename"; Arguments = "args" } + $psobjValue = [PSCustomObject] $hashValue + + ## Test 'Test-ScriptCmdlet -StartInfo' with IDictionary and PSObject with properties + try { + Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $hashValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingArgumentTransformationException,Execute-Script" + } + + try { + Execute-Script -ps $ps -Command "Test-ScriptCmdlet" -ParameterName "StartInfo" -Argument $psobjValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingArgumentTransformationException,Execute-Script" + } + + ## Test 'Test-ScriptFunction -StartInfo' with IDictionary and PSObject with properties + try { + Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $hashValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingArgumentTransformationException,Execute-Script" + } + + try { + Execute-Script -ps $ps -Command "Test-ScriptFunction" -ParameterName "StartInfo" -Argument $psobjValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingArgumentTransformationException,Execute-Script" + } + + ## Test 'Test-BinaryCmdlet -StartInfo' with IDictionary and PSObject with properties + try { + Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $hashValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingException,Execute-Script" + } + + try { + Execute-Script -ps $ps -Command "Test-BinaryCmdlet" -ParameterName "StartInfo" -Argument $psobjValue + throw "Expected exception was not thrown!" + } catch { + $_.FullyQualifiedErrorId | Should Be "ParameterBindingException,Execute-Script" + } + } + finally { + $ps.Dispose() + } + } +} diff --git a/test/powershell/engine/Security/UntrustedDataMode.Tests.ps1 b/test/powershell/engine/Security/UntrustedDataMode.Tests.ps1 new file mode 100644 index 00000000000..ddbdafd2527 --- /dev/null +++ b/test/powershell/engine/Security/UntrustedDataMode.Tests.ps1 @@ -0,0 +1,560 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "UntrustedDataMode tests for variable assignments" -Tags 'CI' { + + BeforeAll { + + $testModule = Join-Path $TestDrive "UntrustedDataModeTest.psm1" + Set-Content -Path $testModule -Value @' + $scriptVar = 15 + $Global:globalVar = "Hello" + + ## A script cmdlet, it goes through CmdletParameterBinderController + function Test-Untrusted + { + [CmdletBinding()] + param( + [Parameter()] + [ValidateTrustedData()] + $Argument + ) + + Write-Output $Argument + } + + ## A simple function, it goes through ScriptParameterBinderController + function Test-SimpleUntrusted + { + param( + [ValidateTrustedData()] + $Argument + ) + + Write-Output $Argument + } + + ## A script cmdlet that tests other parameter types + function Test-OtherParameterType + { + [CmdletBinding()] + param( + [Parameter()] + [ValidateTrustedData()] + [string[]] $Name, + + [Parameter()] + [ValidateTrustedData()] + [DateTime] $Date, + + [Parameter()] + [ValidateTrustedData()] + [System.IO.FileInfo] $File, + + [Parameter()] + [ValidateTrustedData()] + [System.Diagnostics.ProcessStartInfo] $StartInfo + ) + + throw "No Validation Exception!" + } + + function Test-WithScriptVar { Test-Untrusted -Argument $scriptVar } + function Test-WithGlobalVar { Test-Untrusted -Argument $Global:globalVar } + + function Test-SplatScriptVar { Test-Untrusted @scriptVar } + function Test-SplatGlobalVar { Test-Untrusted @Global:globalVar } + + function Get-ScriptVar { $scriptVar } + function Get-GlobalVar { $Global:globalVar } + + function Set-ScriptVar { $Script:scriptVar = "Trusted-Script" } + function Set-GlobalVar { $Global:globalVar = "Trusted-Global" } + + ## + ## ValidateTrustedData attribute is applied to some powershell cmdlets + ## and the functions below are for testing them in FullLanguage + ## + function Test-AddType { Add-Type -TypeDefinition $args[0] } + function Test-InvokeExpression { Invoke-Expression -Command $args[0] } + function Test-NewObject { New-Object -TypeName $args[0] } + function Test-ForeachObject { Get-Date | Foreach-Object -MemberName $args[0] } + function Test-ImportModule { Import-Module -Name $args[0] } + function Test-StartJob { Start-Job -ScriptBlock $args[0] } + +'@ + + ## Use a different runspace + $ps = [powershell]::Create() + + ## Helper function to execute script + function Execute-Script + { + param([string]$Script) + + $ps.Commands.Clear() + $ps.Streams.ClearStreams() + $ps.AddScript($Script).Invoke() + } + + ## Import the module and verify the original behavior of functions + ## exposed from UntrustedDataModeTest in FullLanguage + Execute-Script -Script "Import-Module $testModule" + + ## Set the LanguageMode to be 'ConstrainedLanguage' + Execute-Script -Script '$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"' + + ## Assign the ModuleInfo object to $mo + Execute-Script -Script '$mo = Get-Module UntrustedDataModeTest' + } + + AfterAll { + ## Clean up the powershell object + $ps.Dispose() + + ## Set the LanguageMode to force rebuilding the type conversion cache. + ## This is needed because type conversions that happen in the new powershell runspace with 'ConstrainedLanguage' mode + ## will be put in the type conversion cache, and that may affect the default session. + $ExecutionContext.SessionState.LanguageMode = "FullLanguage" + } + + It "verify the initial state of the test module 'UntrustedDataModeTest'" { + $result = Execute-Script -Script "Test-WithScriptVar" + $result | Should Be 15 + + $result = Execute-Script -Script "Test-WithGlobalVar" + $result | Should Be "Hello" + + $result = Execute-Script -Script "Get-ScriptVar" + $result | Should Be 15 + + $result = Execute-Script -Script "Get-GlobalVar" + $result | Should Be "Hello" + + $result = Execute-Script -Script '$ExecutionContext.SessionState.LanguageMode' + $result | Should Be "ConstrainedLanguage" + } + + Context "Set global variable value in top-level session state" { + + BeforeAll { + + $testScript = @' + Get-GlobalVar + try { Test-WithGlobalVar } catch { $_.FullyQualifiedErrorId } +'@ + + $testCases = @( + ## Assignment in language + @{ + Name = 'language in global scope' + SetupScript = '$globalVar = "language in global scope"' + ExpectedOutput = "language in global scope;ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = 'language in sub scope' + SetupScript = '& { $Global:globalVar = "language in sub scope" }' + ExpectedOutput = "language in sub scope;ParameterArgumentValidationError,Test-Untrusted" + }, + + ## New-Variable + @{ + Name = 'New-Variable in global scope' + SetupScript = 'New-Variable globalVar -Value "New-Variable in global scope" -Force' + ExpectedOutput = "New-Variable in global scope;ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = "New-Variable in sub scope with [-Scope Global]" + SetupScript = '& { New-Variable globalVar -Value "New-Variable in sub scope with [-Scope Global]" -Force -Scope Global }' + ExpectedOutput = "New-Variable in sub scope with [-Scope Global];ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = "New-Variable in sub scope with [-Scope 1]" + SetupScript = '& { New-Variable globalVar -Value "New-Variable in sub scope with [-Scope 1]" -Force -Scope 1 }' + ExpectedOutput = "New-Variable in sub scope with [-Scope 1];ParameterArgumentValidationError,Test-Untrusted" + }, + + ## Set-Variable + @{ + Name = 'Set-Variable in global scope' + SetupScript = 'Set-Variable globalVar -Value "Set-Variable in global scope" -Force' + ExpectedOutput = "Set-Variable in global scope;ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = 'Set-Variable in sub scope with [-Scope Global]' + SetupScript = '& { Set-Variable globalVar -Value "Set-Variable in sub scope with [-Scope Global]" -Scope Global }' + ExpectedOutput = "Set-Variable in sub scope with [-Scope Global];ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = 'Set-Variable in sub scope with [-Scope 1]' + SetupScript = '& { Set-Variable globalVar -Value "Set-Variable in sub scope with [-Scope 1]" -Scope 1 }' + ExpectedOutput = "Set-Variable in sub scope with [-Scope 1];ParameterArgumentValidationError,Test-Untrusted" + }, + + ## New-Item + @{ + Name = 'New-Item in global scope' + SetupScript = 'New-Item variable:\globalVar -Value "New-Item in global scope" -Force' + ExpectedOutput = "New-Item in global scope;ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = 'New-Item in sub scope' + ## New-Item in sub scope won't affect global variable + SetupScript = 'Set-GlobalVar; & { New-Item variable:\globalVar -Value "New-Item in sub scope" -Force }' + ExpectedOutput = "Trusted-Global;Trusted-Global" + }, + + ## Set-Item + @{ + Name = 'Set-Item in global scope' + SetupScript = 'Set-Item variable:\globalVar -Value "Set-Item in global scope" -Force' + ExpectedOutput = "Set-Item in global scope;ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = 'Set-Item in sub scope' + ## Set-Item in sub scope won't affect global variable + SetupScript = 'Set-GlobalVar; & { Set-Item variable:\globalVar -Value "Set-Item in sub scope" -Force }' + ExpectedOutput = "Trusted-Global;Trusted-Global" + }, + + ## Error Variable + @{ + Name = 'ErrorVariable in global scope' + SetupScript = 'Write-Error "Error" -ErrorAction SilentlyContinue -ErrorVariable globalVar' + ExpectedOutput = "Error;ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = 'ErrorVariable in sub scope' + SetupScript = '& { Write-Error "Error-in-Sub" -ErrorAction SilentlyContinue -ErrorVariable global:globalVar }' + ExpectedOutput = "Error-in-Sub;ParameterArgumentValidationError,Test-Untrusted" + }, + + ## Out Variable + @{ + Name = 'OutVariable in global scope' + SetupScript = 'Write-Output "Out" -OutVariable globalVar' + ExpectedOutput = "Out;ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = 'OutVariable in sub scope' + SetupScript = '& { Write-Output "Out-in-Sub" -OutVariable global:globalVar }' + ExpectedOutput = "Out-in-Sub;ParameterArgumentValidationError,Test-Untrusted" + }, + + ## Warning Variable + @{ + Name = 'WarningVariable in global scope' + SetupScript = 'Write-Warning "Warning" -WarningAction SilentlyContinue -WarningVariable globalVar' + ExpectedOutput = "Warning;ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = 'WarningVariable in sub scope' + SetupScript = '& { Write-Warning "Warning-in-Sub" -WarningAction SilentlyContinue -WarningVariable global:globalVar }' + ExpectedOutput = "Warning-in-Sub;ParameterArgumentValidationError,Test-Untrusted" + }, + + ## Information Variable + @{ + Name = 'InformationVariable in global scope' + SetupScript = 'Write-Information "Information" -InformationAction SilentlyContinue -InformationVariable globalVar' + ExpectedOutput = "Information;ParameterArgumentValidationError,Test-Untrusted" + }, + @{ + Name = 'InformationVariable in sub scope' + SetupScript = '& { Write-Information "Information-in-Sub" -InformationAction SilentlyContinue -InformationVariable global:globalVar }' + ExpectedOutput = "Information-in-Sub;ParameterArgumentValidationError,Test-Untrusted" + }, + + ## Data Section + <# @{ + Name = 'Data Section - "data global:var"' + ## 'data global:var { }' syntax is not supported today. If it's added someday, this test should be enabled + SetupScript = '& { data global:globalVar { "data section - [global:]" } }' + ExpectedOutput = "data section - [global:];ParameterArgumentValidationError,Test-Untrusted" + }, #> + @{ + Name = 'Data Section' + SetupScript = 'data globalVar { "data section" }' + ExpectedOutput = "data section;ParameterArgumentValidationError,Test-Untrusted" + } + ) + } + + It "" -TestCases $testCases { + param ($SetupScript, $ExpectedOutput) + + Execute-Script -Script $SetupScript > $null + $result = Execute-Script -Script $testScript + + $result -join ";" | Should Be $ExpectedOutput + } + + It "Enable 'data global:var' test if the syntax is supported" { + try { + [scriptblock]::Create('data global:var { "data section" }') + throw "No Exception!" + } catch { + + ## Syntax 'data global:var { }' is not supported at the time writting the tests here + ## If this test fail, then maybe this syntax is supported now, and in that case, please + ## enable the test 'Data Section - "data global:var"' in $testCases above + $_.FullyQualifiedErrorId | Should Be "ParseException" + } + } + } + + Context "Set variable in Import-LocalizedData" { + + BeforeAll { + $localData = Join-Path $TestDrive "local.psd1" + Set-Content $localData -Value '"Localized-Data"' + } + + It "test global variable set by Import-LocalizedData" { + $testScript = @' + Get-GlobalVar + try { Test-WithGlobalVar } catch { $_.FullyQualifiedErrorId } +'@ + Execute-Script -Script "Import-LocalizedData -BindingVariable globalVar -BaseDirectory $TestDrive -FileName local.psd1" + $result = Execute-Script -Script $testScript + + $result -join ";" | Should Be "Localized-Data;ParameterArgumentValidationError,Test-Untrusted" + } + } + + Context "Exported variables by module loading" { + + BeforeAll { + ## Create a module that exposes two variables + $VarModule = Join-Path $TestDrive "Var.psm1" + Set-Content $VarModule -Value @' + $globalVar = "global-from-module" + $scriptVar = "script-from-module" + Export-ModuleMember -Variable globalVar, scriptVar +'@ + + $testScript = @' + Get-GlobalVar + try { Test-WithGlobalVar } catch { $_.FullyQualifiedErrorId } + + Get-ScriptVar + try { Test-WithScriptVar } catch { $_.FullyQualifiedErrorId } +'@ + } + + BeforeEach { + ## Set both the global and script vars to default value + Execute-Script -Script "Set-ScriptVar; Set-GlobalVar" + } + + It "test global variable set by exported variable" { + try { + ## Import the module in the global scope of the runspace, so only the + ## global variable is affected, the module script variable is not. + Execute-Script -Script "Import-Module $VarModule" + $result = Execute-Script -Script $testScript + + $result -join ";" | Should Be "global-from-module;ParameterArgumentValidationError,Test-Untrusted;Trusted-Script;Trusted-Script" + } finally { + Execute-Script -Script "Remove-Module Var -Force" + } + } + } + + Context "Splatting of untrusted value" { + It "test splatting global variable" { + $testScript = @' + Get-GlobalVar + try { Test-SplatGlobalVar } catch { $_.FullyQualifiedErrorId } +'@ + Execute-Script -Script '$globalVar = @{ Argument = "global-splatting" }' + $result = Execute-Script -Script $testScript + + $result -join ";" | Should Be "System.Collections.Hashtable;ParameterArgumentValidationError,Test-Untrusted" + } + } + + Context "ValidateTrustedDataAttribute takes NO effect in non-FullLanguage" { + + It "test 'ValidateTrustedDataAttribute' NOT take effect in non-FullLanguage [Add-Type]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + try { + $globalVar = "C# Code" + Add-Type -TypeDefinition $globalVar + throw "Expected 'CannotDefineNewType' error was not thrown" + } catch { + $_.FullyQualifiedErrorId + } +'@ + $result | Should Be "CannotDefineNewType,Microsoft.PowerShell.Commands.AddTypeCommand" + } + + It "test 'ValidateTrustedDataAttribute' NOT take effect in non-FullLanguage [Invoke-Expression]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + $globalVar = "gps -id $PID" + Invoke-Expression -Command $globalVar | ForEach-Object Id +'@ + $result | Should Be $PID + } + + It "test 'ValidateTrustedDataAttribute' NOT take effect in non-FullLanguage [New-Object]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + $globalVar = "uri" + New-Object -TypeName $globalVar -ArgumentList 'http://www.bing.com' +'@ + $result | Should Not BeNullOrEmpty + } + + It "test 'ValidateTrustedDataAttribute' NOT take effect in non-FullLanguage [Foreach-Object]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + $globalVar = "Year" + Get-Date | Foreach-Object -MemberName $globalVar +'@ + $result | Should Not BeNullOrEmpty + } + + It "test 'ValidateTrustedDataAttribute' NOT take effect in non-FullLanguage [Import-Module]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + $globalVar = "NonExistModule" + Import-Module -Name $globalVar -ErrorAction SilentlyContinue -ErrorVariable ev; $ev +'@ + $result | Should Not BeNullOrEmpty + $result.FullyQualifiedErrorId | Should Be "Modules_ModuleNotFound,Microsoft.PowerShell.Commands.ImportModuleCommand" + } + + It "test 'ValidateTrustedDataAttribute' NOT take effect in non-FullLanguage [Start-Job]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + try { + $globalVar = {1+1} + Start-Job -ScriptBlock $globalVar + throw "Expected 'CannotStartJob' error was not thrown" + } catch { + $_.FullyQualifiedErrorId + } +'@ + $result | Should Be "CannotStartJobInconsistentLanguageMode,Microsoft.PowerShell.Commands.StartJobCommand" + } + } + + Context "ValidateTrustedDataAttribute takes effect in FullLanguage" { + + It "test 'ValidateTrustedDataAttribute' take effect when calling from 'Constrained' to 'Full' [Script Cmdlet]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + try { + $globalVar = "C# Code" + Test-Untrusted -Argument $globalVar + throw "Expected 'ParameterArgumentValidationError' was not thrown" + } catch { + $_.FullyQualifiedErrorId + } +'@ + $result | Should Be "ParameterArgumentValidationError,Test-Untrusted" + } + + It "test 'ValidateTrustedDataAttribute' take effect when calling from 'Constrained' to 'Full' [Simple function]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + try { + $globalVar = "C# Code" + Test-SimpleUntrusted -Argument $globalVar + throw "Expected 'ParameterArgumentValidationError' was not thrown" + } catch { + $_.FullyQualifiedErrorId + } +'@ + $result | Should Be "ParameterArgumentValidationError,Test-SimpleUntrusted" + } + + It "test 'ValidateTrustedDataAttribute' with param type conversion [string -> string[]]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + try { + $globalVar = "John" + Test-OtherParameterType -Name $globalVar + throw "Expected 'ParameterArgumentValidationError' was not thrown" + } catch { + $_.FullyQualifiedErrorId + } +'@ + $result | Should Be "ParameterArgumentValidationError,Test-OtherParameterType" + } + + It "test 'ValidateTrustedDataAttribute' with value type param [DateTime]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + try { + $globalVar = Get-Date + Test-OtherParameterType -Date $globalVar + throw "Expected 'ParameterArgumentValidationError' was not thrown" + } catch { + $_.FullyQualifiedErrorId + } +'@ + $result | Should Be "ParameterArgumentValidationError,Test-OtherParameterType" + } + + It "test 'ValidateTrustedDataAttribute' with param type conversion [string -> FileInfo]" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + try { + $globalVar = "FakeFile" + Test-OtherParameterType -File $globalVar + throw "Expected 'ParameterArgumentValidationError' was not thrown" + } catch { + $_.FullyQualifiedErrorId + } +'@ + $result | Should Be "ParameterArgumentValidationError,Test-OtherParameterType" + } + + It "test type property conversion to [ProcessStartInfo] should fail during Lang-Mode transition" { + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $result = Execute-Script -Script @' + try { + Test-OtherParameterType -StartInfo @{ FileName = "File" } + throw "Expected conversion error was not thrown" + } catch { + $_.FullyQualifiedErrorId + } +'@ + $result | Should Be "ParameterArgumentTransformationError,Test-OtherParameterType" + } + } + + Context "Validate trusted data for parameters of some built-in powershell cmdlets" { + + BeforeAll { + $ScriptTemplate = @' + try {{ + {0} + throw "Expected 'ParameterArgumentValidationError' was not thrown" + }} catch {{ + $_.FullyQualifiedErrorId + }} +'@ + $testCases = @( + @{ Name = "test 'ValidateTrustedDataAttribute' on [Add-Type]"; Argument = '$globalVar = "Global-Value"; Test-AddType $globalVar'; ExpectedErrorId = "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.AddTypeCommand" } + @{ Name = "test 'ValidateTrustedDataAttribute' on [Invoke-Expression]"; Argument = '$globalVar = "Global-Value"; Test-InvokeExpression $globalVar'; ExpectedErrorId = "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeExpressionCommand" } + @{ Name = "test 'ValidateTrustedDataAttribute' on [New-Object]"; Argument = '$globalVar = "Global-Value"; Test-NewObject $globalVar'; ExpectedErrorId = "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.NewObjectCommand" } + @{ Name = "test 'ValidateTrustedDataAttribute' on [Foreach-Object]"; Argument = '$globalVar = "Global-Value"; Test-ForeachObject $globalVar'; ExpectedErrorId = "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.ForeachObjectCommand" } + @{ Name = "test 'ValidateTrustedDataAttribute' on [Import-Module]"; Argument = '$globalVar = "Global-Value"; Test-ImportModule $globalVar'; ExpectedErrorId = "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.ImportModuleCommand" } + @{ Name = "test 'ValidateTrustedDataAttribute' on [Start-Job]"; Argument = '$globalVar = {1+1}; Test-StartJob $globalVar'; ExpectedErrorId = "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.StartJobCommand" } + ) + } + + It "" -TestCases $testCases { + param ($Argument, $ExpectedErrorId) + ## Run this in the global scope, so value of $globalVar will be marked as untrusted + $testScript = $ScriptTemplate -f $Argument + $result = Execute-Script -Script $testScript + $result | Should Be $ExpectedErrorId + } + } +} From 069664ad16a522f07592a1142270c0fe963b2e10 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Thu, 4 Oct 2018 11:15:12 -0700 Subject: [PATCH 06/13] Configuration keyword bug fix, PSRP protocol version check for reconstruct reconnect, Sharing InitialSessionState in runspace pool. --- .../engine/ExternalScriptInfo.cs | 51 +++++++++++++++++- .../engine/InitialSessionState.cs | 20 ++++--- .../remoting/server/serverremotesession.cs | 5 +- .../engine/Remoting/RunspacePool.Tests.ps1 | Bin 0 -> 5146 bytes .../HelpersRemoting/HelpersRemoting.psd1 | 2 +- .../HelpersRemoting/HelpersRemoting.psm1 | 33 ++++++++++++ 6 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 test/powershell/engine/Remoting/RunspacePool.Tests.ps1 diff --git a/src/System.Management.Automation/engine/ExternalScriptInfo.cs b/src/System.Management.Automation/engine/ExternalScriptInfo.cs index 21fbdd71436..4b9293cbec3 100644 --- a/src/System.Management.Automation/engine/ExternalScriptInfo.cs +++ b/src/System.Management.Automation/engine/ExternalScriptInfo.cs @@ -211,7 +211,7 @@ public ScriptBlock ScriptBlock } // parse the script into an expression tree... - ScriptBlock newScriptBlock = ScriptBlock.Create(new Parser(), _path, ScriptContents); + ScriptBlock newScriptBlock = ParseScriptContents(new Parser(), _path, ScriptContents, DefiningLanguageMode); this.ScriptBlock = newScriptBlock; } @@ -229,6 +229,31 @@ private set private ScriptBlock _scriptBlock; private ScriptBlockAst _scriptBlockAst; + private static ScriptBlock ParseScriptContents(Parser parser, string fileName, string fileContents, PSLanguageMode? definingLanguageMode) + { + // If we are in ConstrainedLanguage mode but the defining language mode is FullLanguage, then we need + // to parse the script contents in FullLanguage mode context. Otherwise we will get bogus parsing errors + // such as "Configuration keyword not allowed". + if (definingLanguageMode.HasValue && (definingLanguageMode == PSLanguageMode.FullLanguage)) + { + var context = LocalPipeline.GetExecutionContextFromTLS(); + if ((context != null) && (context.LanguageMode == PSLanguageMode.ConstrainedLanguage)) + { + context.LanguageMode = PSLanguageMode.FullLanguage; + try + { + return ScriptBlock.Create(parser, fileName, fileContents); + } + finally + { + context.LanguageMode = PSLanguageMode.ConstrainedLanguage; + } + } + } + + return ScriptBlock.Create(parser, fileName, fileContents); + } + internal ScriptBlockAst GetScriptBlockAst() { var scriptContents = ScriptContents; @@ -244,7 +269,29 @@ internal ScriptBlockAst GetScriptBlockAst() { ParseError[] errors; Parser parser = new Parser(); - _scriptBlockAst = parser.Parse(_path, ScriptContents, null, out errors, ParseMode.Default); + + // If we are in ConstrainedLanguage mode but the defining language mode is FullLanguage, then we need + // to parse the script contents in FullLanguage mode context. Otherwise we will get bogus parsing errors + // such as "Configuration keyword not allowed". + var context = LocalPipeline.GetExecutionContextFromTLS(); + if (context != null && context.LanguageMode == PSLanguageMode.ConstrainedLanguage && + DefiningLanguageMode == PSLanguageMode.FullLanguage) + { + context.LanguageMode = PSLanguageMode.FullLanguage; + try + { + _scriptBlockAst = parser.Parse(_path, ScriptContents, null, out errors, ParseMode.Default); + } + finally + { + context.LanguageMode = PSLanguageMode.ConstrainedLanguage; + } + } + else + { + _scriptBlockAst = parser.Parse(_path, ScriptContents, null, out errors, ParseMode.Default); + } + if (errors.Length == 0) { this.ScriptBlock = new ScriptBlock(_scriptBlockAst, isFilter: false); diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index d449aa09b47..ca6f6660e9d 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -2374,7 +2374,8 @@ internal Exception BindRunspace(Runspace initializedRunspace, PSTraceSource runs // If a user has any module with the same name as that of the core module( or nested module inside the core module) // in his module path, then that will get loaded instead of the actual nested module (from the GAC - in our case) // Hence, searching only from the system module path while loading the core modules - ProcessImportModule(initializedRunspace, CoreModulesToImport, ModuleIntrinsics.GetPSHomeModulePath(), publicCommands); + var unresolvedCmdsToExpose = new HashSet(this.UnresolvedCommandsToExpose, StringComparer.OrdinalIgnoreCase); + ProcessImportModule(initializedRunspace, CoreModulesToImport, ModuleIntrinsics.GetPSHomeModulePath(), publicCommands, unresolvedCmdsToExpose); // Win8:328748 - functions defined in global scope end up in a module // Since we import the core modules, EngineSessionState's module is set to the last imported module. So, if a function is defined in global scope, it ends up in that module. @@ -2384,7 +2385,7 @@ internal Exception BindRunspace(Runspace initializedRunspace, PSTraceSource runs // Set the SessionStateDrive here since we have all the provider information at this point SetSessionStateDrive(initializedRunspace.ExecutionContext, true); - Exception moduleImportException = ProcessImportModule(initializedRunspace, ModuleSpecificationsToImport, string.Empty, publicCommands); + Exception moduleImportException = ProcessImportModule(initializedRunspace, ModuleSpecificationsToImport, string.Empty, publicCommands, unresolvedCmdsToExpose); if (moduleImportException != null) { runspaceInitTracer.WriteLine( @@ -2394,10 +2395,10 @@ internal Exception BindRunspace(Runspace initializedRunspace, PSTraceSource runs // If we still have unresolved commands after importing specified modules, then try finding associated module for // each unresolved command and import that module. - string[] foundModuleList = GetModulesForUnResolvedCommands(UnresolvedCommandsToExpose, initializedRunspace.ExecutionContext); + string[] foundModuleList = GetModulesForUnResolvedCommands(unresolvedCmdsToExpose, initializedRunspace.ExecutionContext); if (foundModuleList.Length > 0) { - ProcessImportModule(initializedRunspace, foundModuleList, string.Empty, publicCommands); + ProcessImportModule(initializedRunspace, foundModuleList, string.Empty, publicCommands, unresolvedCmdsToExpose); } ProcessDynamicVariables(initializedRunspace); @@ -2807,7 +2808,12 @@ private Exception ProcessPowerShellCommand(PowerShell psToInvoke, Runspace initi return null; } - private RunspaceOpenModuleLoadException ProcessImportModule(Runspace initializedRunspace, IEnumerable moduleList, string path, HashSet publicCommands) + private RunspaceOpenModuleLoadException ProcessImportModule( + Runspace initializedRunspace, + IEnumerable moduleList, + string path, + HashSet publicCommands, + HashSet unresolvedCmdsToExpose) { RunspaceOpenModuleLoadException exceptionToReturn = null; @@ -2877,7 +2883,7 @@ private RunspaceOpenModuleLoadException ProcessImportModule(Runspace initialized if (exceptionToReturn == null) { // Now go through the list of commands not yet resolved to ensure they are public if requested - foreach (string unresolvedCommand in UnresolvedCommandsToExpose.ToArray()) + foreach (string unresolvedCommand in unresolvedCmdsToExpose.ToArray()) { string moduleName; string commandToMakeVisible = Utils.ParseCommandName(unresolvedCommand, out moduleName); @@ -2910,7 +2916,7 @@ private RunspaceOpenModuleLoadException ProcessImportModule(Runspace initialized if (found && !WildcardPattern.ContainsWildcardCharacters(commandToMakeVisible)) { - UnresolvedCommandsToExpose.Remove(unresolvedCommand); + unresolvedCmdsToExpose.Remove(unresolvedCommand); } } } diff --git a/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs b/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs index 89d9508742c..b841b8a590c 100644 --- a/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs +++ b/src/System.Management.Automation/engine/remoting/server/serverremotesession.cs @@ -1005,8 +1005,9 @@ private bool RunServerNegotiationAlgorithm(RemoteSessionCapability clientCapabil // Win10 server can support reconstruct/reconnect for all 2.x protocol versions // that support reconstruct/reconnect, Protocol 2.2+ // Major protocol version differences (2.x -> 3.x) are not supported. - if ((serverProtocolVersion == RemotingConstants.ProtocolVersionWin10RTM) && - (clientProtocolVersion.Major == serverProtocolVersion.Major)) + // A reconstruct can only be initiated by a client that understands disconnect (2.2+), + // so we only need to check major versions from client and this server for compatibility. + if (clientProtocolVersion.Major == RemotingConstants.ProtocolVersion.Major) { if (clientProtocolVersion.Minor == RemotingConstants.ProtocolVersionWin8RTM.Minor) { diff --git a/test/powershell/engine/Remoting/RunspacePool.Tests.ps1 b/test/powershell/engine/Remoting/RunspacePool.Tests.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..30afde824c6b552af1423d27ea274f920ed83cad GIT binary patch literal 5146 zcmdT|Sx*yD6h6-;{)Z7K0ix4{<%z@xte`cDL=YZ~549bjp$m3ekf?uM{l0U0xwCY- zn9@j^c4qE9_blI8r+|AE@XhUHEGB?uJ5tdz)nvRe9v@`^nIM* zZyhTw+;yb_>RoB$6QnpL`V`}IjMZ!WHbMDV9tkDCQ|>WNbH9m|A?}+>8-Vi#?x;ge zjtkVrg`8^A8EG?+2-H1r7(x%8eS_aNb}2oPkGOWQPo2B4m8bcAh4n=3Y{^&L2eOi) z<#S&(OTeiPIAai&Cl)J!!G3j#4`6GV~LCGv-y`Yz= z%el&=fBSQyM2)>~vcC80vmv;zSzuWn$sTlJ{p%wu7<=Zg0C<^Sn7wVFY#y}AGK2-w z?PsQ0HHu+9E1$0~hmdI1h1j^bN&Bn(1btE#O&*K!7>$eGUzt3!r!?KJ_67QEp1M(k zwb{D#FLlG$q1n)kfHJeeN8fmamincNara& z!5T`w|GZgTPULNv7F{*u7kj`7>||YLhh*in`G8%}PYUUpXuZ)oae|Zz`xYJj2e%S&7qv^$nhLRn8`(X`QOPZ%1P(Jz&|)p-J2}N+vyWDPmg2Wpe_T zBC2IvwkB{X;#$UKd&H$2o621m4&*1WKFa({5~;`b@Lvi4Pq8GyMRJ*I8xLkRR TTQ_Aa!#>9Neao{++ Date: Thu, 4 Oct 2018 14:22:56 -0700 Subject: [PATCH 07/13] Restricted remote session in UMCI, Applocker detection collision, Help command exposing functions, Null reference exception fix. --- .../host/msh/ConsoleHost.cs | 5 +- .../engine/InitialSessionState.cs | 42 +++-- .../engine/SessionStateFunctionAPIs.cs | 2 +- .../engine/Utils.cs | 42 +++++ .../engine/debugger/debugger.cs | 3 +- .../engine/hostifaces/RunspacePoolInternal.cs | 5 +- .../server/ServerRunspacePoolDriver.cs | 6 +- .../engine/runtime/Operations/VariableOps.cs | 87 ++++++----- .../security/wldpNativeMethods.cs | 11 +- .../ConstrainedLanguageRestriction.Tests.ps1 | 143 ++++++++++++++++++ 10 files changed, 269 insertions(+), 77 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index bcbec704f41..2541435d203 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs @@ -1657,10 +1657,7 @@ private void DoRunspaceInitialization(bool skipProfiles, string initialCommand, // If the system lockdown policy says "Enforce", do so. Do this after types / formatting, default functions, etc // are loaded so that they are trusted. (Validation of their signatures is done in F&O) - if (SystemPolicy.GetSystemLockdownPolicy() == SystemEnforcementMode.Enforce) - { - _runspaceRef.Runspace.ExecutionContext.LanguageMode = PSLanguageMode.ConstrainedLanguage; - } + Utils.EnforceSystemLockDownLanguageMode(_runspaceRef.Runspace.ExecutionContext); string allUsersProfile = HostUtilities.GetFullProfileFileName(null, false); string allUsersHostSpecificProfile = HostUtilities.GetFullProfileFileName(shellId, false); diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index ca6f6660e9d..ad5d32f06d0 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -12,6 +12,7 @@ using System.Management.Automation.Internal; using System.Management.Automation.Provider; using System.Management.Automation.Language; +using System.Management.Automation.Security; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; @@ -761,6 +762,13 @@ internal SessionStateFunctionEntry(string name, string definition, ScopedItemOpt HelpFile = helpFile; } + internal static SessionStateFunctionEntry GetDelayParsedFunctionEntry(string name, string definition, bool isProductCode, PSLanguageMode languageMode) + { + var fnEntry = GetDelayParsedFunctionEntry(name, definition, isProductCode); + fnEntry.ScriptBlock.LanguageMode = languageMode; + return fnEntry; + } + internal static SessionStateFunctionEntry GetDelayParsedFunctionEntry(string name, string definition, bool isProductCode) { var sb = ScriptBlock.CreateDelayParsedScriptBlock(definition, isProductCode); @@ -4734,21 +4742,28 @@ internal static SessionStateAliasEntry[] BuiltInAliases internal const string DefaultSetDriveFunctionText = "Set-Location $MyInvocation.MyCommand.Name"; internal static ScriptBlock SetDriveScriptBlock = ScriptBlock.CreateDelayParsedScriptBlock(DefaultSetDriveFunctionText, isProductCode: true); + private static PSLanguageMode systemLanguageMode = (SystemPolicy.GetSystemLockdownPolicy() == SystemEnforcementMode.Enforce) ? PSLanguageMode.ConstrainedLanguage : PSLanguageMode.FullLanguage; internal static SessionStateFunctionEntry[] BuiltInFunctions = new SessionStateFunctionEntry[] { - // Functions. Only the name and definitions are used - SessionStateFunctionEntry.GetDelayParsedFunctionEntry("prompt", DefaultPromptFunctionText, isProductCode: true), - SessionStateFunctionEntry.GetDelayParsedFunctionEntry("TabExpansion2", s_tabExpansionFunctionText, isProductCode: true), - SessionStateFunctionEntry.GetDelayParsedFunctionEntry("Clear-Host", GetClearHostFunctionText(), isProductCode: true), - SessionStateFunctionEntry.GetDelayParsedFunctionEntry("help", GetHelpPagingFunctionText(), isProductCode: true), - // Porting note: we remove mkdir on Linux because it is a conflict + // Functions that don't require full language mode + SessionStateFunctionEntry.GetDelayParsedFunctionEntry("cd..", "Set-Location ..", isProductCode: true, languageMode: systemLanguageMode), + SessionStateFunctionEntry.GetDelayParsedFunctionEntry("cd\\", "Set-Location \\", isProductCode: true, languageMode: systemLanguageMode), + // Win8: 320909. Retaining the original definition to ensure backward compatability. + SessionStateFunctionEntry.GetDelayParsedFunctionEntry("Pause", + string.Concat("$null = Read-Host '", CodeGeneration.EscapeSingleQuotedStringContent(RunspaceInit.PauseDefinitionString),"'"), isProductCode: true, languageMode: systemLanguageMode), + SessionStateFunctionEntry.GetDelayParsedFunctionEntry("help", GetHelpPagingFunctionText(), isProductCode: true, languageMode: systemLanguageMode), + SessionStateFunctionEntry.GetDelayParsedFunctionEntry("prompt", DefaultPromptFunctionText, isProductCode: true, languageMode: systemLanguageMode), + + // Functions that require full language mode and are trusted + SessionStateFunctionEntry.GetDelayParsedFunctionEntry("Clear-Host", GetClearHostFunctionText(), isProductCode: true, languageMode: PSLanguageMode.FullLanguage), + SessionStateFunctionEntry.GetDelayParsedFunctionEntry("TabExpansion2", s_tabExpansionFunctionText, isProductCode: true, languageMode: PSLanguageMode.FullLanguage), + SessionStateFunctionEntry.GetDelayParsedFunctionEntry("oss", GetOSTFunctionText(), isProductCode: true, languageMode: PSLanguageMode.FullLanguage), #if !UNIX - SessionStateFunctionEntry.GetDelayParsedFunctionEntry("mkdir", GetMkdirFunctionText(), isProductCode: true), + // Porting note: we remove mkdir on Linux because of a conflict + SessionStateFunctionEntry.GetDelayParsedFunctionEntry("mkdir", GetMkdirFunctionText(), isProductCode: true, languageMode: PSLanguageMode.FullLanguage), #endif - SessionStateFunctionEntry.GetDelayParsedFunctionEntry("oss", GetOSTFunctionText(), isProductCode: true), - - // Porting note: we remove the drive functions from Linux because they make no sense #if !UNIX + // Porting note: we remove the drive functions from Linux because they make no sense in that environment // Default drives SessionStateFunctionEntry.GetDelayParsedFunctionEntry("A:", DefaultSetDriveFunctionText, SetDriveScriptBlock), SessionStateFunctionEntry.GetDelayParsedFunctionEntry("B:", DefaultSetDriveFunctionText, SetDriveScriptBlock), @@ -4775,13 +4790,8 @@ internal static SessionStateAliasEntry[] BuiltInAliases SessionStateFunctionEntry.GetDelayParsedFunctionEntry("W:", DefaultSetDriveFunctionText, SetDriveScriptBlock), SessionStateFunctionEntry.GetDelayParsedFunctionEntry("X:", DefaultSetDriveFunctionText, SetDriveScriptBlock), SessionStateFunctionEntry.GetDelayParsedFunctionEntry("Y:", DefaultSetDriveFunctionText, SetDriveScriptBlock), - SessionStateFunctionEntry.GetDelayParsedFunctionEntry("Z:", DefaultSetDriveFunctionText, SetDriveScriptBlock), + SessionStateFunctionEntry.GetDelayParsedFunctionEntry("Z:", DefaultSetDriveFunctionText, SetDriveScriptBlock) #endif - - SessionStateFunctionEntry.GetDelayParsedFunctionEntry("cd..", "Set-Location ..", isProductCode: true), - SessionStateFunctionEntry.GetDelayParsedFunctionEntry("cd\\", "Set-Location \\", isProductCode: true), - SessionStateFunctionEntry.GetDelayParsedFunctionEntry("Pause", - string.Concat("$null = Read-Host '", CodeGeneration.EscapeSingleQuotedStringContent(RunspaceInit.PauseDefinitionString),"'"), isProductCode: true) }; internal static void RemoveAllDrivesForProvider(ProviderInfo pi, SessionStateInternal ssi) diff --git a/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs b/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs index 71147018ea1..9d4e3100128 100644 --- a/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs +++ b/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs @@ -27,7 +27,7 @@ internal void AddSessionStateEntry(SessionStateFunctionEntry entry) FunctionInfo fn = this.SetFunction(entry.Name, sb, null, entry.Options, false, CommandOrigin.Internal, this.ExecutionContext, entry.HelpFile, true); fn.Visibility = entry.Visibility; fn.Module = entry.Module; - fn.ScriptBlock.LanguageMode = PSLanguageMode.FullLanguage; + fn.ScriptBlock.LanguageMode = entry.ScriptBlock.LanguageMode ?? PSLanguageMode.FullLanguage; } /// diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index a940f3aaac2..a4f58a6a207 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -1362,6 +1362,48 @@ internal static bool IsComObject(object obj) #endif } + /// + /// EnforceSystemLockDownLanguageMode + /// FullLangauge -> ConstrainedLanguage + /// RestrictedLanguage -> NoLanguage + /// ConstrainedLanguage -> ConstrainedLanguage + /// NoLanguage -> NoLanguage + /// + /// ExecutionContext + /// Previous language mode or null for no language mode change + internal static PSLanguageMode? EnforceSystemLockDownLanguageMode(ExecutionContext context) + { + PSLanguageMode? oldMode = null; + + if (SystemPolicy.GetSystemLockdownPolicy() == SystemEnforcementMode.Enforce) + { + switch (context.LanguageMode) + { + case PSLanguageMode.FullLanguage: + oldMode = context.LanguageMode; + context.LanguageMode = PSLanguageMode.ConstrainedLanguage; + break; + + case PSLanguageMode.RestrictedLanguage: + oldMode = context.LanguageMode; + context.LanguageMode = PSLanguageMode.NoLanguage; + break; + + case PSLanguageMode.ConstrainedLanguage: + case PSLanguageMode.NoLanguage: + break; + + default: + Diagnostics.Assert(false, "Unexpected PSLanguageMode"); + oldMode = context.LanguageMode; + context.LanguageMode = PSLanguageMode.NoLanguage; + break; + } + } + + return oldMode; + } + #region Implicit Remoting Batching // Commands allowed to run on target remote session along with implicit remote commands diff --git a/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index a3be0cdebbf..476a1729b9f 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -1754,8 +1754,7 @@ private void OnDebuggerStop(InvocationInfo invocationInfo, List brea System.Management.Automation.Security.SystemEnforcementMode.Enforce) { // If there is a system lockdown in place, enforce it - originalLanguageMode = _context.LanguageMode; - _context.LanguageMode = PSLanguageMode.ConstrainedLanguage; + originalLanguageMode = Utils.EnforceSystemLockDownLanguageMode(this._context); } RunspaceAvailability previousAvailability = _context.CurrentRunspace.RunspaceAvailability; diff --git a/src/System.Management.Automation/engine/hostifaces/RunspacePoolInternal.cs b/src/System.Management.Automation/engine/hostifaces/RunspacePoolInternal.cs index 328c4a8f419..2309146a6a0 100644 --- a/src/System.Management.Automation/engine/hostifaces/RunspacePoolInternal.cs +++ b/src/System.Management.Automation/engine/hostifaces/RunspacePoolInternal.cs @@ -1263,10 +1263,7 @@ protected Runspace CreateRunspace() result.Open(); // Enforce the system lockdown policy if one is defined. - if (SystemPolicy.GetSystemLockdownPolicy() == SystemEnforcementMode.Enforce) - { - result.ExecutionContext.LanguageMode = PSLanguageMode.ConstrainedLanguage; - } + Utils.EnforceSystemLockDownLanguageMode(result.ExecutionContext); result.Events.ForwardEvent += OnRunspaceForwardEvent; // this must be done after open since open initializes the ExecutionContext diff --git a/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs b/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs index b136837a353..79a43bb42b9 100644 --- a/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs +++ b/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs @@ -632,11 +632,7 @@ private void HandleRunspaceCreated(object sender, RunspaceCreatedEventArgs args) // If the system lockdown policy says "Enforce", do so (unless it's in the // more restrictive NoLanguage mode) - if ((SystemPolicy.GetSystemLockdownPolicy() == SystemEnforcementMode.Enforce) && - (args.Runspace.ExecutionContext.LanguageMode != PSLanguageMode.NoLanguage)) - { - args.Runspace.ExecutionContext.LanguageMode = PSLanguageMode.ConstrainedLanguage; - } + Utils.EnforceSystemLockDownLanguageMode(args.Runspace.ExecutionContext); // Set the current location to MyDocuments folder for this runspace. // This used to be set to the Personal folder but was changed to MyDocuments folder for diff --git a/src/System.Management.Automation/engine/runtime/Operations/VariableOps.cs b/src/System.Management.Automation/engine/runtime/Operations/VariableOps.cs index 5167156a13d..ca420c2a905 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/VariableOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/VariableOps.cs @@ -46,6 +46,8 @@ internal static object SetVariableValue(VariablePath variablePath, object value, : GetAttributeCollection(attributeAsts); var = new PSVariable(variablePath.UnqualifiedPath, value, ScopedItemOptions.None, attributes); + // Marking untrusted values for assignments in 'ConstrainedLanguage' mode is done in + // SessionStateScope.SetVariable. sessionState.SetVariable(variablePath, var, false, origin); if (executionContext._debuggingMode > 0) @@ -53,56 +55,59 @@ internal static object SetVariableValue(VariablePath variablePath, object value, executionContext.Debugger.CheckVariableWrite(variablePath.UnqualifiedPath); } } - else if (attributeAsts != null) + else { - // Use bytewise operation directly instead of 'var.IsReadOnly || var.IsConstant' on - // a hot path (setting variable with type constraint) to get better performance. - if ((var.Options & (ScopedItemOptions.ReadOnly | ScopedItemOptions.Constant)) != ScopedItemOptions.None) + if (attributeAsts != null) { - SessionStateUnauthorizedAccessException e = - new SessionStateUnauthorizedAccessException( - var.Name, - SessionStateCategory.Variable, - "VariableNotWritable", - SessionStateStrings.VariableNotWritable); - throw e; - } + // Use bytewise operation directly instead of 'var.IsReadOnly || var.IsConstant' on + // a hot path (setting variable with type constraint) to get better performance. + if ((var.Options & (ScopedItemOptions.ReadOnly | ScopedItemOptions.Constant)) != ScopedItemOptions.None) + { + SessionStateUnauthorizedAccessException e = + new SessionStateUnauthorizedAccessException( + var.Name, + SessionStateCategory.Variable, + "VariableNotWritable", + SessionStateStrings.VariableNotWritable); + throw e; + } - var attributes = GetAttributeCollection(attributeAsts); - value = PSVariable.TransformValue(attributes, value); - if (!PSVariable.IsValidValue(attributes, value)) + var attributes = GetAttributeCollection(attributeAsts); + value = PSVariable.TransformValue(attributes, value); + if (!PSVariable.IsValidValue(attributes, value)) + { + ValidationMetadataException e = new ValidationMetadataException( + "ValidateSetFailure", + null, + Metadata.InvalidValueFailure, + var.Name, + ((value != null) ? value.ToString() : "$null")); + + throw e; + } + var.SetValueRaw(value, true); + // Don't update the PSVariable's attributes until we successfully set the value + var.Attributes.Clear(); + var.AddParameterAttributesNoChecks(attributes); + + if (executionContext._debuggingMode > 0) + { + executionContext.Debugger.CheckVariableWrite(variablePath.UnqualifiedPath); + } + } + else { - ValidationMetadataException e = new ValidationMetadataException( - "ValidateSetFailure", - null, - Metadata.InvalidValueFailure, - var.Name, - ((value != null) ? value.ToString() : "$null")); - - throw e; + // The setter will handle checking for variable writes. + var.Value = value; } - var.SetValueRaw(value, true); - // Don't update the PSVariable's attributes until we successfully set the value - var.Attributes.Clear(); - var.AddParameterAttributesNoChecks(attributes); - if (executionContext._debuggingMode > 0) + if (executionContext.LanguageMode == PSLanguageMode.ConstrainedLanguage) { - executionContext.Debugger.CheckVariableWrite(variablePath.UnqualifiedPath); + // Mark untrusted values for assignments to 'Global:' variables, and 'Script:' variables in + // a module scope, if it's necessary. + ExecutionContext.MarkObjectAsUntrustedForVariableAssignment(var, scope, sessionState); } } - else - { - // The setter will handle checking for variable writes. - var.Value = value; - } - - if (executionContext.LanguageMode == PSLanguageMode.ConstrainedLanguage) - { - // Mark untrusted values for assignments to 'Global:' variables, and 'Script:' variables in - // a module scope, if it's necessary. - ExecutionContext.MarkObjectAsUntrustedForVariableAssignment(var, scope, sessionState); - } return value; } diff --git a/src/System.Management.Automation/security/wldpNativeMethods.cs b/src/System.Management.Automation/security/wldpNativeMethods.cs index cac346dfb17..34989c73c0f 100644 --- a/src/System.Management.Automation/security/wldpNativeMethods.cs +++ b/src/System.Management.Automation/security/wldpNativeMethods.cs @@ -174,6 +174,8 @@ private static SystemEnforcementMode GetWldpPolicy(string path, SafeHandle handl } private static SystemEnforcementMode? s_cachedWldpSystemPolicy = null; + private const string AppLockerTestFileName = "__PSScriptPolicyTest_"; + private const string AppLockerTestFileContents = "# PowerShell test file to determine AppLocker lockdown mode "; private static SystemEnforcementMode GetAppLockerPolicy(string path, SafeHandle handle) { SaferPolicy result = SaferPolicy.Disallowed; @@ -216,13 +218,14 @@ private static SystemEnforcementMode GetAppLockerPolicy(string path, SafeHandle IO.Directory.CreateDirectory(tempPath); } - testPathScript = IO.Path.Combine(tempPath, IO.Path.GetRandomFileName() + ".ps1"); - testPathModule = IO.Path.Combine(tempPath, IO.Path.GetRandomFileName() + ".psm1"); + testPathScript = IO.Path.Combine(tempPath, AppLockerTestFileName + IO.Path.GetRandomFileName() + ".ps1"); + testPathModule = IO.Path.Combine(tempPath, AppLockerTestFileName + IO.Path.GetRandomFileName() + ".psm1"); // AppLocker fails when you try to check a policy on a file // with no content. So create a scratch file and test on that. - IO.File.WriteAllText(testPathScript, "1"); - IO.File.WriteAllText(testPathModule, "1"); + String dtAppLockerTestFileContents = AppLockerTestFileContents + DateTime.Now; + IO.File.WriteAllText(testPathScript, dtAppLockerTestFileContents); + IO.File.WriteAllText(testPathModule, dtAppLockerTestFileContents); } catch (System.IO.IOException) { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 index 117c21aebb1..957543b0432 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 @@ -18,6 +18,149 @@ try $defaultParamValues = $PSDefaultParameterValues.Clone() $PSDefaultParameterValues["it:Skip"] = !$IsWindows + Describe "Help built-in function should not expose nested module private functions when run on locked down systems" -Tags 'Feature','RequireAdminOnWindows' { + + BeforeAll { + + $restorePSModulePath = $env:PSModulePath + $env:PSModulePath += ";$TestDrive" + + $trustedModuleName1 = "TrustedModule$(Get-Random -Max 999)_System32" + $trustedModulePath1 = Join-Path $TestDrive $trustedModuleName1 + mkdir $trustedModulePath1 + $trustedModuleFilePath1 = Join-Path $trustedModulePath1 ($trustedModuleName1 + ".psm1") + $trustedModuleManifestPath1 = Join-Path $trustedModulePath1 ($trustedModuleName1 + ".psd1") + + $trustedModuleName2 = "TrustedModule$(Get-Random -Max 999)_System32" + $trustedModulePath2 = Join-Path $TestDrive $trustedModuleName2 + mkdir $trustedModulePath2 + $trustedModuleFilePath2 = Join-Path $trustedModulePath2 ($trustedModuleName2 + ".psm1") + + $trustedModuleScript1 = @' + function PublicFn1 + { + NestedFn1 + PrivateFn1 + } + function PrivateFn1 + { + "PrivateFn1" + } +'@ + $trustedModuleScript1 | Out-File -FilePath $trustedModuleFilePath1 + '@{{ FunctionsToExport = "PublicFn1"; ModuleVersion = "1.0"; RootModule = "{0}"; NestedModules = "{1}" }}' -f @($trustedModuleFilePath1,$trustedModuleName2) | Out-File -FilePath $trustedModuleManifestPath1 + + $trustedModuleScript2 = @' + function NestedFn1 + { + "NestedFn1" + "Language mode is $($ExecutionContext.SessionState.LanguageMode)" + } +'@ + $trustedModuleScript2 | Out-File -FilePath $trustedModuleFilePath2 + } + + AfterAll { + + $env:PSModulePath = $restorePSModulePath + if ($trustedModuleName1 -ne $null) { Remove-Module -Name $trustedModuleName1 -Force -ErrorAction Ignore } + if ($trustedModuleName2 -ne $null) { Remove-Module -Name $trustedModuleName2 -Force -ErrorAction Ignore } + } + + It "Verifies that private functions in trusted nested modules are not globally accessible after running the help function" { + + $isCommandAccessible = "False" + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + $command = @" + Import-Module -Name $trustedModuleName1 -Force -ErrorAction Stop; +"@ + $command += @' + $null = help NestedFn1 2>$null; + $result = Get-Command NestedFn1 2>$null; + return ($result -ne $null) +'@ + $isCommandAccessible = powershell.exe -noprofile -nologo -c $command + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + # Verify that nested function NestedFn1 was not accessible + $isCommandAccessible | Should -BeExactly "False" + } + } + + Describe "NoLanguage runspace pool session should remain in NoLanguage mode when created on a system-locked down machine" -Tags 'Feature','RequireAdminOnWindows' { + + BeforeAll { + + $configFileName = "RestrictedSessionConfig.pssc" + $configFilePath = Join-Path $TestDrive $configFileName + '@{ SchemaVersion = "2.0.0.0"; SessionType = "RestrictedRemoteServer"}' > $configFilePath + + $scriptModuleName = "ImportTrustedModuleForTest_System32" + $moduleFilePath = Join-Path $TestDrive ($scriptModuleName + ".psm1") + $template = @' + function TestRestrictedSession + {{ + $iss = [initialsessionstate]::CreateFromSessionConfigurationFile("{0}") + $rsp = [runspacefactory]::CreateRunspacePool($iss) + $rsp.Open() + $ps = [powershell]::Create() + $ps.RunspacePool = $rsp + $null = $ps.AddScript("Hello") + + try + {{ + $ps.Invoke() + }} + finally + {{ + $ps.Dispose() + $rsp.Dispose() + }} + }} + + Export-ModuleMember -Function TestRestrictedSession +'@ + $template -f $configFilePath > $moduleFilePath + } + + It "Verifies that a NoLanguage runspace pool throws the expected 'script not allowed' error" { + + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + $mod = Import-Module -Name $moduleFilePath -Force -PassThru + + # Running module function TestRestrictedSession should throw a 'script not allowed' error + # because it runs in a 'no language' session. + try + { + & "$scriptModuleName\TestRestrictedSession" + throw "No Exception!" + } + catch + { + $expectedError = $_ + } + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + $expectedError.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should -BeExactly "ScriptsNotAllowed" + } + } + Describe "Built-ins work within constrained language" -Tags 'Feature','RequireAdminOnWindows' { BeforeAll { From 401cfe2d66e43ad4b26e2c231fabbf7b2450e921 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Mon, 22 Oct 2018 13:33:37 -0700 Subject: [PATCH 08/13] Added mitigation for debugger function exposure --- .../engine/FunctionInfo.cs | 1 + .../engine/SessionStateFunctionAPIs.cs | 34 ++- .../engine/debugger/debugger.cs | 30 ++- .../ConstrainedLanguageDebugger.Tests.ps1 | 242 ++++++++++++++++++ 4 files changed, 292 insertions(+), 15 deletions(-) diff --git a/src/System.Management.Automation/engine/FunctionInfo.cs b/src/System.Management.Automation/engine/FunctionInfo.cs index 0bf51497fd4..309fd374f00 100644 --- a/src/System.Management.Automation/engine/FunctionInfo.cs +++ b/src/System.Management.Automation/engine/FunctionInfo.cs @@ -190,6 +190,7 @@ public ScriptBlock ScriptBlock internal void Update(ScriptBlock newFunction, bool force, ScopedItemOptions options) { Update(newFunction, force, options, null); + this.DefiningLanguageMode = newFunction.LanguageMode; } /// diff --git a/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs b/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs index 9d4e3100128..1dcf2f143fa 100644 --- a/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs +++ b/src/System.Management.Automation/engine/SessionStateFunctionAPIs.cs @@ -165,9 +165,41 @@ internal FunctionInfo GetFunction(string name, CommandOrigin origin) { result = ((IEnumerator)searcher).Current; } - return result; + + return (IsFunctionVisibleInDebugger(result, origin)) ? result : null; } // GetFunction + private bool IsFunctionVisibleInDebugger(FunctionInfo fnInfo, CommandOrigin origin) + { + // Ensure the returned function item is not exposed across language boundaries when in + // a debugger breakpoint or nested prompt. + // A debugger breakpoint/nested prompt has access to all current scoped functions. + // This includes both running commands from the prompt or via a debugger Action scriptblock. + + // Early out. + // Always allow built-in functions needed for command line debugging. + if ((this.ExecutionContext.LanguageMode == PSLanguageMode.FullLanguage) || + (fnInfo == null) || + (fnInfo.Name.Equals("prompt", StringComparison.OrdinalIgnoreCase)) || + (fnInfo.Name.Equals("TabExpansion2", StringComparison.OrdinalIgnoreCase)) || + (fnInfo.Name.Equals("Clear-Host", StringComparison.Ordinal))) + { + return true; + } + + // Check both InNestedPrompt and Debugger.InBreakpoint to ensure we don't miss a case. + // Function is not visible if function and context language modes are different. + var runspace = this.ExecutionContext.CurrentRunspace; + if ((runspace != null) && + (runspace.InNestedPrompt || (runspace.Debugger?.InBreakpoint == true)) && + (fnInfo.DefiningLanguageMode.HasValue && (fnInfo.DefiningLanguageMode != this.ExecutionContext.LanguageMode))) + { + return false; + } + + return true; + } + /// /// Get a functions out of session state. /// diff --git a/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index 476a1729b9f..c926110ac47 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -1725,6 +1725,22 @@ private void OnDebuggerStop(InvocationInfo invocationInfo, List brea // Ignore, it means they don't have the default prompt } + // Change the context language mode before updating the prompt script. + // This way the new prompt scriptblock will pick up the current context language mode. + PSLanguageMode? originalLanguageMode = null; + if (_context.UseFullLanguageModeInDebugger && + (_context.LanguageMode != PSLanguageMode.FullLanguage)) + { + originalLanguageMode = _context.LanguageMode; + _context.LanguageMode = PSLanguageMode.FullLanguage; + } + else if (System.Management.Automation.Security.SystemPolicy.GetSystemLockdownPolicy() == + System.Management.Automation.Security.SystemEnforcementMode.Enforce) + { + // If there is a system lockdown in place, enforce it + originalLanguageMode = Utils.EnforceSystemLockDownLanguageMode(this._context); + } + // Update the prompt to the debug prompt if (hadDefaultPrompt) { @@ -1743,20 +1759,6 @@ private void OnDebuggerStop(InvocationInfo invocationInfo, List brea } } - PSLanguageMode? originalLanguageMode = null; - if (_context.UseFullLanguageModeInDebugger && - (_context.LanguageMode != PSLanguageMode.FullLanguage)) - { - originalLanguageMode = _context.LanguageMode; - _context.LanguageMode = PSLanguageMode.FullLanguage; - } - else if (System.Management.Automation.Security.SystemPolicy.GetSystemLockdownPolicy() == - System.Management.Automation.Security.SystemEnforcementMode.Enforce) - { - // If there is a system lockdown in place, enforce it - originalLanguageMode = Utils.EnforceSystemLockDownLanguageMode(this._context); - } - RunspaceAvailability previousAvailability = _context.CurrentRunspace.RunspaceAvailability; _context.CurrentRunspace.UpdateRunspaceAvailability( diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 index 1258d72f55e..f3422574bfe 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 @@ -175,6 +175,248 @@ try $debuggerTester.ScriptException | Should -BeNullOrEmpty } } + + Describe "Cross language debugger get-item commands should not have access to FullLanguage trusted functions through provider" -Tags 'Feature','RequireAdminOnWindows' { + + BeforeAll { + + # Trusted module that will always run in FullLanguage mode + $scriptModuleName = "ImportTrustedModuleForTestA_System32" + $moduleFilePath = Join-Path $TestDrive ($scriptModuleName + ".psm1") + $script = @' + function PublicFn { "PublicFn"; PrivateFn } + function PrivateFn { "PrivateFn" } + + Export-ModuleMember -Function PublicFn +'@ + $script > $moduleFilePath + + # Import and run module function script + $scriptIM = @' + Import-Module -Name {0} -Force + $null = Set-PSBreakpoint -command PublicFn + PublicFn +'@ -f $moduleFilePath + + # Debugger stop event handler object. + $type = @' + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Management.Automation; + using System.Management.Automation.Runspaces; + + public class DebuggerStopEventHandler + { + private Runspace _runspace; + public object GetItemResult + { + get; + internal set; + } + public object GetChildItemResult + { + get; + internal set; + } + public object CopyItemResult + { + get; + internal set; + } + public object FunctionVariableResult + { + get; + internal set; + } + public object RenameItemResult + { + get; + internal set; + } + public DebuggerStopEventHandler(Runspace runspace) + { + _runspace = runspace; + _runspace.Debugger.DebuggerStop += (sender, args) => + { + var debugger = sender as Debugger; + + PSDataCollection output = new PSDataCollection(); + PSCommand command = new PSCommand(); + + command.AddScript(@"Get-Item -Path function:\PrivateFn 2>&1"); + debugger.ProcessCommand(command, output); + GetItemResult = (output.Count > 0) ? (output[0].BaseObject) : null; + + command.Clear(); + output.Clear(); + command.AddScript(@"Get-ChildItem -Path function:\PrivateFn 2>&1"); + debugger.ProcessCommand(command, output); + GetChildItemResult = (output.Count > 0) ? (output[0].BaseObject) : null; + + command.Clear(); + output.Clear(); + command.AddScript(@"Copy-Item -Path function:\PrivateFn -Destination function:\MyPrivateFn 2>&1"); + debugger.ProcessCommand(command, output); + CopyItemResult = (output.Count > 0) ? (output[0].BaseObject) : null; + + command.Clear(); + output.Clear(); + command.AddScript(@"${function:\PrivateFn}"); + debugger.ProcessCommand(command, output); + FunctionVariableResult = (output.Count > 0) ? (output[0].BaseObject): null; + + command.Clear(); + output.Clear(); + command.AddScript(@"Rename-Item -Path function:\PrivateFn -NewName function:\MyPrivateFn -Passthru 2>&1"); + debugger.ProcessCommand(command, output); + RenameItemResult = (output.Count > 0) ? (output[0].BaseObject) : null; + }; + } + public void Reset() { GetItemResult = null; GetChildItemResult = null; CopyItemResult = null; } + } +'@ + + try { Add-Type -TypeDefinition $type } catch { } + + # Create runspace and debugger event handler + [runspace] $rs = [runspacefactory]::CreateRunspace($host) + $rs.Open() + $rs.Debugger.SetDebugMode(@('LocalScript','RemoteScript')) + $debuggerStopHandler = [DebuggerStopEventHandler]::New($rs) + + # Create PowerShell to run module script + [powershell] $ps = [powershell]::Create() + $ps.Runspace = $rs + $ps.AddScript($scriptIM) + } + + AfterAll { + + if ($rs -ne $null) { $rs.Dispose() } + if ($ps -ne $null) { $ps.Dispose() } + } + + It "Verifies that same language mode trusted public functions *are* accessible from debugger through Get-Item, Get-ChildItem, Copy-Item, Rename-Item, Variable" { + + # Test + $results = $ps.Invoke() + + # Results. Only PublicFn is returned since PrivateFn is renamed. + $results[0] | Should Be "PublicFn" + + # Expected Get-Item function:\PrivateFn returns FunctionInfo object + ($debuggerStopHandler.GetItemResult -is [System.Management.Automation.FunctionInfo]) | Should Be $true + + # Expected Get-ChildItem function:\PrivateFn returns FunctionInfo object + ($debuggerStopHandler.GetChildItemResult -is [System.Management.Automation.FunctionInfo]) | Should Be $true + + # Expected Copy-Item function:\PrivateFn succeeds with no error output + $debuggerStopHandler.CopyItemResult | Should Be $null + + # Expected function variable succeeds + ($debuggerStopHandler.FunctionVariableResult -is [scriptblock]) | Should Be $true + + # Expected Rename-Item function:\PrivateFn returns FunctionInfo object + ($debuggerStopHandler.RenameItemResult -is [System.Management.Automation.FunctionInfo]) | Should Be $true + } + + It "Verifies that cross language mode trusted public functions *are not* accessible through Get-Item, Get-ChildItem, Copy-Item, Rename-Item, Variable" { + + # Test + $debuggerStopHandler.Reset() + try + { + $rs.LanguageMode = "ConstrainedLanguage" + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + + $results = $ps.Invoke() + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + # Results + $results[0] | Should Be "PublicFn" + $results[1] | Should Be "PrivateFn" + + # Expected Get-Item function:\PrivateFn returns error + $debuggerStopHandler.GetItemResult.FullyQualifiedErrorId | Should Be "PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand" + + # Expected Get-ChildItem function:\PrivateFn returns error + $debuggerStopHandler.GetChildItemResult.FullyQualifiedErrorId | Should Be "PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand" + + # Expected Copy-Item fails with error output + $debuggerStopHandler.CopyItemResult.FullyQualifiedErrorId | Should Be "PathNotFound,Microsoft.PowerShell.Commands.CopyItemCommand" + + # Expected function variable fails + $debuggerStopHandler.FunctionVariableResult | Should Be $null + + # Expected Rename-Item function:\PrivateFn fails with error + $debuggerStopHandler.RenameItemResult.FullyQualifiedErrorId | Should Be "InvalidOperation,Microsoft.PowerShell.Commands.RenameItemCommand" + } + } + + Describe "Cross language debugger Action scripts should not have access to FullLanguage trusted functions through provider" -Tags 'Feature','RequireAdminOnWindows' { + + BeforeAll { + + # Trusted script that will always run in FullLanguage mode + $scriptFileName = "TrustedScriptForTestB_System32" + $scriptFilePath = Join-Path $TestDrive ($scriptFileName + ".ps1") + $script = @' + function PublicFn { PrivateFn -typeDef 'public class Hello { public new static void ToString() { System.Console.WriteLine("Hello!"); } }'; [Hello]::ToString(); } + function PrivateFn { param ([string]$typeDef) Add-Type -TypeDefinition $typeDef } + PublicFn + "Complete" +'@ + $script > $scriptFilePath + } + + AfterAll { + + Get-PSBreakpoint | Remove-PSBreakpoint + } + + It "Verifies that debugger stop Action scriptblock cannot access PrivateFn" { + + try + { + Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode + $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" + + # Set breakpoint on script + Set-PSBreakpoint -Script $scriptFilePath -Line 4 -Action { + & (Get-Item -Path function:\PrivateFn) -typeDef @' + public class Foo { + public new static void ToString() { + System.Console.WriteLine("pwnd!"); + } + } +'@ + } + + # Run script + & $scriptFilePath + } + finally + { + Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode + } + + try + { + # Verify that Action scriptblock did not create Foo type using PrivateFn + [Foo]::ToString() + throw "No Exception!" + } + catch + { + $_.FullyQualifiedErrorId | Should Be "TypeNotFound" + } + } + } } finally { From 65f53a569b2a9de86856211bc9d55eb044f89dad Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 13 Nov 2018 11:05:06 -0800 Subject: [PATCH 09/13] remove file to change encoding --- .../engine/Remoting/RunspacePool.Tests.ps1 | Bin 5146 -> 0 bytes .../Modules/HelpersSecurity/HelpersSecurity.psd1 | Bin 756 -> 0 bytes .../Modules/HelpersSecurity/HelpersSecurity.psm1 | Bin 5462 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/powershell/engine/Remoting/RunspacePool.Tests.ps1 delete mode 100644 test/tools/Modules/HelpersSecurity/HelpersSecurity.psd1 delete mode 100644 test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 diff --git a/test/powershell/engine/Remoting/RunspacePool.Tests.ps1 b/test/powershell/engine/Remoting/RunspacePool.Tests.ps1 deleted file mode 100644 index 30afde824c6b552af1423d27ea274f920ed83cad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5146 zcmdT|Sx*yD6h6-;{)Z7K0ix4{<%z@xte`cDL=YZ~549bjp$m3ekf?uM{l0U0xwCY- zn9@j^c4qE9_blI8r+|AE@XhUHEGB?uJ5tdz)nvRe9v@`^nIM* zZyhTw+;yb_>RoB$6QnpL`V`}IjMZ!WHbMDV9tkDCQ|>WNbH9m|A?}+>8-Vi#?x;ge zjtkVrg`8^A8EG?+2-H1r7(x%8eS_aNb}2oPkGOWQPo2B4m8bcAh4n=3Y{^&L2eOi) z<#S&(OTeiPIAai&Cl)J!!G3j#4`6GV~LCGv-y`Yz= z%el&=fBSQyM2)>~vcC80vmv;zSzuWn$sTlJ{p%wu7<=Zg0C<^Sn7wVFY#y}AGK2-w z?PsQ0HHu+9E1$0~hmdI1h1j^bN&Bn(1btE#O&*K!7>$eGUzt3!r!?KJ_67QEp1M(k zwb{D#FLlG$q1n)kfHJeeN8fmamincNara& z!5T`w|GZgTPULNv7F{*u7kj`7>||YLhh*in`G8%}PYUUpXuZ)oae|Zz`xYJj2e%S&7qv^$nhLRn8`(X`QOPZ%1P(Jz&|)p-J2}N+vyWDPmg2Wpe_T zBC2IvwkB{X;#$UKd&H$2o621m4&*1WKFa({5~;`b@Lvi4Pq8GyMRJ*I8xLkRR TTQ_Aa!#>9Neao{++c=Mrh=SIU696HF|IQ`Eo+QmpMofPyK1Xbi@MgM zZq;95Jh^-!xDL`)3#LPjbd2Tdh|>XKZV?fH|i0mBM9Hw}^}glRh_PC)=zu`0NEU}voo2AX9rzYLozI+z)2MD%xh5!Hn diff --git a/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 b/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 deleted file mode 100644 index 4ab90c4a8b90da5bd8c12707755d6d1eb9c2bfca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5462 zcmdUzYi}Dx6o$`dB>uxHvD8LlO}~L?sai;YNc5sOz=yV~Tzlh~#$L-_C$v%jI`F(R zo@Ca$>#k`CLaSY`XU?3?+t@~SY6W9=ZD4!+K4EOYOl}MA z6E`RNIV<$+Gcx9FYF(%w+Zk69aay>48LKl!&v_1^{MN&eY!&seSbAtOuPhmJOZ z^C@rA;n0pN)SintjnPXQcc5F?Z~PaJ+%EXH=VV>FX9AV3Lz}F$FD!+74)qefCF>`i zXUt05g+pcnwd|85-GNI7T(nZFzU6*ocPccyaLK^bBb3-L@RW8V=!d*%cHj^cGKQ6IW$`w3e^iay=#s z6eoQbF^ZkGE1$4t$~|JWG?F%5a&*aW&d7D{dX8~)?>hRSlN{oAJp;ZpU5P!wnkD?i zaSmP&kul^_?)SOntqXpIr7}xzxqJHje$431Md%>XDY5;Pmdcn1NLrT?>-&OSogh=u zR!|Fe9Qx;Qc+1F+eSy3k>WvT?p{+;7=YB?Ykw@e?&Gwj=MyhW`iSG{e7Kwtev%HVWdq*&j0p$DLeS<@wMgt6I6g`cTd^om zmo4kz!7*zFL}QgpVh7mQcSvOR1DGi9o9v7=4btj04zr42?Mj#2%7p?6b0j9bx0ib# z`VY0p1Ud3wkh2YEzXP0uz!{olWN4G5digV~851AMNae@J(B4M3YHkN>|IK(%wJV5# zoQQc#HfT4VuJ+oj)(8JI0=&ldn)yUGZmB?LK->>^o?*r%$QmTItUt;bf{E zVS5jTGx#=@sa{UV%gyy5Q1!4yJFGup_psmB&~D@I<9(~$vuS_oHtt~F2SN7wudnTO z*agaf(&o&*30w~FuFjhB@6_%0{`4oj3b$KVXMY7d#Y@=7b&7h;<*_PP^5^hXPch(3 z7fx6A><|0e?nT#EtGRk@*5w^L`F^kOvRCxoX+v?E5tBECeVbhSe|liLI(O-;ds~Qy z$PT~d^Ns&^hu&CO5oboogcR?pcBy|?XQz%_eWULt)W?_4&S_)TwmriqNH`$Ql^Jt7 zmFvD%l15G+#mL@T9jvcM9W=fEe(;o-)!EwDm|)KlqyCG;gvv4|Z!!y%F(v`SGS7XN z#lDA@#_v|#e@CuzBc)SO%{yhhbJfZ0L#Y#GW-m)$IMj=j`ESys&PxI3)b#;DZj3@z z<8>^9rS+JG*$&qEPQv5)6l*T9_YiB<(aMwI{rIZ4tV&3AZJy-a86n}9_`JSOZUR_#26l)I@ChA2MA!Q#|Eq-|w zDRB?!F;M>LON}h+J1umkSl=zXjQOv9?eeJ(p$};4dw%tm>hGFy-t^Tjy~?NCv^=)R I>+PR^10B&f9{>OV From 3270d37c366161eefafccf44b4d5be8cb13edfe8 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 13 Nov 2018 11:06:08 -0800 Subject: [PATCH 10/13] recommit file with UTF8 encoding --- .../engine/Remoting/RunspacePool.Tests.ps1 | 71 +++++++++++++++ .../HelpersSecurity/HelpersSecurity.psd1 | 13 +++ .../HelpersSecurity/HelpersSecurity.psm1 | 89 +++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 test/powershell/engine/Remoting/RunspacePool.Tests.ps1 create mode 100644 test/tools/Modules/HelpersSecurity/HelpersSecurity.psd1 create mode 100644 test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 diff --git a/test/powershell/engine/Remoting/RunspacePool.Tests.ps1 b/test/powershell/engine/Remoting/RunspacePool.Tests.ps1 new file mode 100644 index 00000000000..10909afe8cb --- /dev/null +++ b/test/powershell/engine/Remoting/RunspacePool.Tests.ps1 @@ -0,0 +1,71 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Import-Module HelpersRemoting + +Describe "Remote runspace pool should expose commands in endpoint configuration" -Tags 'Feature','RequireAdminOnWindows' { + + BeforeAll { + + if ($isWindows) + { + $configName = "restrictedV" + $configPath = Join-Path $TestDrive ($configName + ".pssc") + + New-PSSessionConfigurationFile -Path $configPath -SessionType RestrictedRemoteServer -VisibleCmdlets 'Get-CimInstance' + + $null = Register-PSSessionConfiguration -Name $configName -Path $configPath -Force -ErrorAction SilentlyContinue + + $remoteRunspacePool = New-RemoteRunspacePool -ConfigurationName $configName + } + } + + AfterAll { + + if ($IsWindows) + { + if ($remoteRunspacePool -ne $null) + { + $remoteRunspacePool.Dispose() + } + + Unregister-PSSessionConfiguration -Name $configName -Force -ErrorAction SilentlyContinue + } + } + + It "Verifies that the configured endpoint cmdlet is available in all runspace pool instances" -Skip:(! $IsWindows) { + + [powershell] $ps1 = [powershell]::Create() + $ps1.RunspacePool = $remoteRunspacePool + $null = $ps1.AddCommand('Get-Command').AddParameter('Name','Get-CimInstance') + + [powershell] $ps2 = [powershell]::Create() + $ps2.RunspacePool = $remoteRunspacePool + $null = $ps2.AddCommand('Get-Command').AddParameter('Name','Get-CimInstance') + + [powershell] $ps3 = [powershell]::Create() + $ps3.RunspacePool = $remoteRunspacePool + $null = $ps3.AddCommand('Get-Command').AddParameter('Name','Get-CimInstance') + + [powershell] $ps4 = [powershell]::Create() + $ps4.RunspacePool = $remoteRunspacePool + $null = $ps4.AddCommand('Get-Command').AddParameter('Name','Get-CimInstance') + + # Invoke all four simultaneously + $a1 = $ps1.BeginInvoke() + $a2 = $ps2.BeginInvoke() + $a3 = $ps3.BeginInvoke() + $a4 = $ps4.BeginInvoke() + + # Wait for completion + $r1 = $ps1.EndInvoke($a1) + $r2 = $ps2.EndInvoke($a2) + $r3 = $ps3.EndInvoke($a3) + $r4 = $ps4.EndInvoke($a4) + + $r1.Name | Should -BeExactly 'Get-CimInstance' + $r2.Name | Should -BeExactly 'Get-CimInstance' + $r3.Name | Should -BeExactly 'Get-CimInstance' + $r4.Name | Should -BeExactly 'Get-CimInstance' + } +} diff --git a/test/tools/Modules/HelpersSecurity/HelpersSecurity.psd1 b/test/tools/Modules/HelpersSecurity/HelpersSecurity.psd1 new file mode 100644 index 00000000000..a812dbcf7ce --- /dev/null +++ b/test/tools/Modules/HelpersSecurity/HelpersSecurity.psd1 @@ -0,0 +1,13 @@ +# +# Manifest for 'HelpersSecurity' module +# + +@{ + RootModule = 'HelpersSecurity.psm1' + ModuleVersion = '1.0' + GUID = '544d00d4-e3b7-46e2-a6a1-8bbf53980e5d' + CompanyName = 'Microsoft Corporation' + Copyright = 'Copyright (c) Microsoft Corporation. All rights reserved.' + Description = 'Security tests helper functions' + FunctionsToExport = @() +} diff --git a/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 b/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 new file mode 100644 index 00000000000..92195b7f358 --- /dev/null +++ b/test/tools/Modules/HelpersSecurity/HelpersSecurity.psm1 @@ -0,0 +1,89 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +if ($IsWindows) +{ + Import-Module HelpersCommon + + $code = @' + + #region Using directives + + using System; + using System.Globalization; + using System.Reflection; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Security; + using System.Runtime.InteropServices; + using System.Threading; + using System.Management.Automation; + + #endregion + + /// Adds a new type to the Application Domain + [Cmdlet("Invoke", "LanguageModeTestingSupportCmdlet")] + public sealed class InvokeLanguageModeTestingSupportCmdlet : PSCmdlet + { + [Parameter()] + public SwitchParameter EnableFullLanguageMode + { + get { return enableFullLanguageMode; } + set { enableFullLanguageMode = value; } + } + private SwitchParameter enableFullLanguageMode; + + [Parameter()] + public SwitchParameter SetLockdownMode + { + get { return setLockdownMode; } + set { setLockdownMode = value; } + } + private SwitchParameter setLockdownMode; + + [Parameter()] + public SwitchParameter RevertLockdownMode + { + get { return revertLockdownMode; } + set { revertLockdownMode = value; } + } + private SwitchParameter revertLockdownMode; + + protected override void BeginProcessing() + { + if (enableFullLanguageMode) + { + SessionState.LanguageMode = PSLanguageMode.FullLanguage; + } + + if (setLockdownMode) + { + Environment.SetEnvironmentVariable("__PSLockdownPolicy", "0x80000007", EnvironmentVariableTarget.Machine); + } + + if (revertLockdownMode) + { + Environment.SetEnvironmentVariable("__PSLockdownPolicy", null, EnvironmentVariableTarget.Machine); + } + } + } +'@ + + if (-not (Get-Command Invoke-LanguageModeTestingSupportCmdlet -ErrorAction Ignore)) + { + $moduleName = Get-RandomFileName + $moduleDirectory = join-path $TestDrive\Modules $moduleName + if (-not (Test-Path $moduleDirectory)) + { + $null = New-Item -ItemType Directory $moduleDirectory -Force + } + + try + { + Add-Type -TypeDefinition $code -OutputAssembly $moduleDirectory\TestCmdletForConstrainedLanguage.dll -ErrorAction Ignore + } catch {} + + Import-Module -Name $moduleDirectory\TestCmdletForConstrainedLanguage.dll + } +} From 3d236774ebbe6d4899672dca65ada4fcbcc400f9 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Tue, 13 Nov 2018 13:44:53 -0800 Subject: [PATCH 11/13] Fix TypeAccelerator test number of types. --- test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 index 747ccaae0a3..28f45901ea6 100644 --- a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 +++ b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 @@ -410,7 +410,7 @@ Describe "Type accelerators" -Tags "CI" { } else { - $totalAccelerators = 103 + $totalAccelerators = 104 $extraFullPSAcceleratorTestCases = @( @{ From 6515ba26ae067c6c0c29b5b897b716368920823b Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Tue, 13 Nov 2018 14:16:58 -0800 Subject: [PATCH 12/13] Fixed TypeAccelerator test number of types for non-Windows platforms --- test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 index 28f45901ea6..e90e404602e 100644 --- a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 +++ b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 @@ -406,7 +406,7 @@ Describe "Type accelerators" -Tags "CI" { if ( !$IsWindows ) { - $totalAccelerators = 98 + $totalAccelerators = 99 } else { From 285a63de0429c181abe02450f12ce34391d469c5 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Tue, 13 Nov 2018 15:14:26 -0800 Subject: [PATCH 13/13] Updated test to look for new expected error type --- .../ConstrainedLanguageDebugger.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 index f3422574bfe..c91074aab76 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageDebugger.Tests.ps1 @@ -354,7 +354,7 @@ try $debuggerStopHandler.FunctionVariableResult | Should Be $null # Expected Rename-Item function:\PrivateFn fails with error - $debuggerStopHandler.RenameItemResult.FullyQualifiedErrorId | Should Be "InvalidOperation,Microsoft.PowerShell.Commands.RenameItemCommand" + $debuggerStopHandler.RenameItemResult.FullyQualifiedErrorId | Should Be "PathNotFound,Microsoft.PowerShell.Commands.RenameItemCommand" } }