diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs index 5e8055572a3..30792f3938b 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs @@ -164,6 +164,12 @@ public abstract partial class WebRequestPSCmdlet : PSCmdlet [Parameter] public virtual SwitchParameter SkipCertificateCheck { get; set; } + /// + /// gets or sets the CertificateValidationScript property. This will be ignored if SkipCertificateCheck is set. + /// + [Parameter] + public virtual ScriptBlock CertificateValidationScript { get; set; } + /// /// Gets or sets the TLS/SSL protocol used by the Web Cmdlet /// diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebRequestPSCmdlet.CoreClr.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebRequestPSCmdlet.CoreClr.cs index 0163b5f0958..acfad4388d5 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebRequestPSCmdlet.CoreClr.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebRequestPSCmdlet.CoreClr.cs @@ -4,15 +4,18 @@ using System; using System.Management.Automation; +using System.Management.Automation.Runspaces; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Net.Security; using System.IO; using System.Text; using System.Collections; using System.Globalization; using System.Security.Authentication; using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Xml; using System.Collections.Generic; @@ -173,6 +176,45 @@ internal virtual HttpClient GetHttpClient(bool handleRedirect) handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; handler.ClientCertificateOptions = ClientCertificateOption.Manual; } + else if (CertificateValidationScript != null) + { + // validationCallBackWrapper wraps the CertificateValidationScript ScriptBlock so the async callback properly execute ScriptBlock + Func validationCallBackWrapper = + delegate(HttpRequestMessage httpRequestMessage, X509Certificate2 x509Certificate2, X509Chain x509Chain, SslPolicyErrors sslPolicyErrors) + { + InitialSessionState iss = InitialSessionState.CreateDefault(); + + // Add $_ variable with the Delegate parameters as members + PSObject dollarUnderbar = new PSObject(); + dollarUnderbar.Members.Add(new PSNoteProperty("Request", httpRequestMessage)); + dollarUnderbar.Members.Add(new PSNoteProperty("Certificate", x509Certificate2)); + dollarUnderbar.Members.Add(new PSNoteProperty("CertificateChain", x509Chain)); + dollarUnderbar.Members.Add(new PSNoteProperty("SslErrors", sslPolicyErrors)); + iss.Variables.Add(new SessionStateVariableEntry(name: "_", value: dollarUnderbar, description: String.Empty)); + + Boolean result = false; + try + { + using (Runspace rs = RunspaceFactory.CreateRunspace(iss)) + using (var ps = System.Management.Automation.PowerShell.Create()) + { + ps.Runspace = rs; + rs.Open(); + ps.AddScript(CertificateValidationScript.ToString()); + + result = LanguagePrimitives.IsTrue(ps.Invoke().First()); + } + } + catch // Treat all exceptions as Certificate failures. + { + result = false; + } + + return result; + }; + + handler.ServerCertificateCustomValidationCallback = validationCallBackWrapper; + } // This indicates GetResponse will handle redirects. if (handleRedirect) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index dc3bdce61ed..5e5b5ba8669 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -1557,6 +1557,58 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { } + Context "Invoke-WebRequest CertificateValidationScript Tests" { + It "Verifies Invoke-WebRequest -CertificateValidationScript can accept all certificates" -Pending:$PendingCertificateTest { + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https + ErrorAction = 'Stop' + CertificateValidationScript = { $true } + } + # WebListener uses a self-signed cert. Without -SkipCertificateCheck this would normally fail + $result = Invoke-WebRequest @Params + $jsonResult = $result.Content | ConvertFrom-Json + $jsonResult.Headers.Host | Should BeExactly $params.Uri.Authority + } + + It "Verifies Invoke-WebRequest -CertificateValidationScript is ignored when -SkipCertificateCheck is present" { + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https + ErrorAction = 'Stop' + SkipCertificateCheck = $true + # This script would fail all certificates + CertificateValidationScript = { $false } + } + $result = Invoke-WebRequest @Params + $jsonResult = $result.Content | ConvertFrom-Json + $jsonResult.Headers.Host | Should BeExactly $params.Uri.Authority + } + + It 'Verifies Invoke-WebRequest -CertificateValidationScript script has a $_' -Pending:$PendingCertificateTest { + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https + ErrorAction = 'Stop' + CertificateValidationScript = { + $_.Certificate.Thumbprint -eq 'C8747A1C4A46E52EEC688A6766967010F86C58E3' -and + $_.CertificateChain.ChainElements[0].Certificate.Thumbprint -eq 'C8747A1C4A46E52EEC688A6766967010F86C58E3' -and + $_.SslErrors -eq 'RemoteCertificateChainErrors' -and + $_.Request.Method.Method -eq 'GET' + } + } + $result = Invoke-WebRequest @Params + $jsonResult = $result.Content | ConvertFrom-Json + $jsonResult.Headers.Host | Should BeExactly $params.Uri.Authority + } + + It "Verifies Invoke-WebRequest -CertificateValidationScript treats exceptions as Certificate failures" -Pending:$PendingCertificateTest { + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https + ErrorAction = 'Stop' + CertificateValidationScript = { throw 'Bad Cert' } + } + { Invoke-WebRequest @Params } | ShouldBeErrorId 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand' + } + } + BeforeEach { if ($env:http_proxy) { $savedHttpProxy = $env:http_proxy @@ -2629,6 +2681,56 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { } } + Context "Invoke-RestMethod CertificateValidationScript Tests" { + It "Verifies Invoke-RestMethod -CertificateValidationScript can accept all certificates" -Pending:$PendingCertificateTest { + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https + ErrorAction = 'Stop' + CertificateValidationScript = { $true } + } + # WebListener uses a self-signed cert. Without -SkipCertificateCheck this would normally fail + $result = Invoke-RestMethod @Params + $result.Headers.Host | Should BeExactly $params.Uri.Authority + } + + It "Verifies Invoke-RestMethod -CertificateValidationScript is ignored when -SkipCertificateCheck is present" { + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https + ErrorAction = 'Stop' + SkipCertificateCheck = $true + # This script would fail all certificates + CertificateValidationScript = { $false } + } + $result = Invoke-RestMethod @Params + $result.Headers.Host | Should BeExactly $params.Uri.Authority + } + + It 'Verifies Invoke-RestMethod -CertificateValidationScript script has a $_' -Pending:$PendingCertificateTest { + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https + ErrorAction = 'Stop' + CertificateValidationScript = { + $WebListenerThumbprint = 'C8747A1C4A46E52EEC688A6766967010F86C58E3' + $_.Certificate.Thumbprint -eq $WebListenerThumbprint -and + $_.CertificateChain.ChainElements[0].Certificate.Thumbprint -eq $WebListenerThumbprint -and + $_.SslErrors -eq 'RemoteCertificateChainErrors' -and + $_.Request.Method.Method -eq 'GET' + } + } + $result = Invoke-RestMethod @Params + $result.Headers.Host | Should BeExactly $params.Uri.Authority + } + + It "Verifies Invoke-RestMethod -CertificateValidationScript treats exceptions as Certificate failures" -Pending:$PendingCertificateTest { + $params = @{ + Uri = Get-WebListenerUrl -Test 'Get' -Https + ErrorAction = 'Stop' + CertificateValidationScript = { throw 'Bad Cert' } + } + { Invoke-RestMethod @Params } | ShouldBeErrorId 'WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand' + } + } + BeforeEach { if ($env:http_proxy) { $savedHttpProxy = $env:http_proxy