From 3a61f77fb844f7c1443f47132109a08a592ab3eb Mon Sep 17 00:00:00 2001 From: Mark Kraus Date: Thu, 9 Nov 2017 14:13:21 -0600 Subject: [PATCH 1/3] [Feature] Add error on Legacy Credential over non-HTTPS for Web Cmdlets --- .spelling | 1 + .../Common/WebRequestPSCmdlet.Common.cs | 6 + .../WebCmdlets.Tests.ps1 | 142 ++++++++++++++++++ .../Modules/WebListener/WebListener.psm1 | 1 + .../WebListener/Controllers/AuthController.cs | 73 +++++++++ test/tools/WebListener/README.md | 70 +++++++++ .../tools/WebListener/Views/Home/Index.cshtml | 3 + 7 files changed, 296 insertions(+) create mode 100644 test/tools/WebListener/Controllers/AuthController.cs diff --git a/.spelling b/.spelling index 802cdb665f7..731c52a50f1 100644 --- a/.spelling +++ b/.spelling @@ -1239,5 +1239,6 @@ v6.0. #region test/tools/WebListener/README.md Overrides - test/tools/WebListener/README.md +NTLM ResponseHeaders #endregion 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 69542eb16ef..1b142f273f4 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 @@ -354,6 +354,12 @@ internal virtual void ValidateParameters() "WebCmdletAllowUnencryptedAuthenticationRequiredException"); ThrowTerminatingError(error); } + if (!AllowUnencryptedAuthentication && (null != Credential || UseDefaultCredentials) && (Uri.Scheme != "https")) + { + ErrorRecord error = GetValidationError(WebCmdletStrings.AllowUnencryptedAuthenticationRequired, + "WebCmdletAllowUnencryptedAuthenticationRequiredException"); + ThrowTerminatingError(error); + } // credentials if (UseDefaultCredentials && (null != Credential)) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index 8dc1b1aaaef..94b0bc54082 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -1312,6 +1312,10 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { $credential = [pscredential]::new("testuser",$token) $httpUri = Get-WebListenerUrl -Test 'Get' $httpsUri = Get-WebListenerUrl -Test 'Get' -Https + $httpBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' + $httpsBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' -Https + $httpNegotiateUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Negotiate' + $httpsNegotiateUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Negotiate' -Https $testCases = @( @{Authentication = "bearer"} @{Authentication = "OAuth"} @@ -1412,6 +1416,75 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { $result.Headers.Authorization | Should BeExactly "Bearer testpassword" } + + It "Verifies Invoke-WebRequest Negotiated -Credential over HTTPS" { + $params = @{ + Uri = $httpsBasicUri + Credential = $credential + SkipCertificateCheck = $true + } + $Response = Invoke-WebRequest @params + $result = $response.Content | ConvertFrom-Json + + $result.Headers.Authorization | Should BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" + } + + It "Verifies Invoke-WebRequest Negotiated -Credential Requires HTTPS" { + $params = @{ + Uri = $httpBasicUri + Credential = $credential + ErrorAction = 'Stop' + } + { Invoke-WebRequest @params } | ShouldBeErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" + } + + It "Verifies Invoke-WebRequest Negotiated -Credential Can use HTTP with -AllowUnencryptedAuthentication" { + $params = @{ + Uri = $httpBasicUri + Credential = $credential + AllowUnencryptedAuthentication = $true + } + $Response = Invoke-WebRequest @params + $result = $response.Content | ConvertFrom-Json + + $result.Headers.Authorization | Should BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" + } + + # UseDefaultCredentials is only reliably testable on Windows + It "Verifies Invoke-WebRequest Negotiated -UseDefaultCredentials over HTTPS" -Skip:$(!$IsWindows) { + $params = @{ + Uri = $httpsNegotiateUri + Credential = $credential + SkipCertificateCheck = $true + } + $Response = Invoke-WebRequest @params + $result = $response.Content | ConvertFrom-Json + + $result.Headers.Authorization | Should Match '^Negotiate ' + } + + # The error condition can at least be tested on all platforms. + It "Verifies Invoke-WebRequest Negotiated -UseDefaultCredentials Requires HTTPS" { + $params = @{ + Uri = $httpNegotiateUri + UseDefaultCredentials = $true + ErrorAction = 'Stop' + } + { Invoke-WebRequest @params } | ShouldBeErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand" + } + + # UseDefaultCredentials is only reliably testable on Windows + It "Verifies Invoke-WebRequest Negotiated -UseDefaultCredentials Can use HTTP with -AllowUnencryptedAuthentication" -Skip:$(!$IsWindows) { + $params = @{ + Uri = $httpNegotiateUri + UseDefaultCredentials = $true + AllowUnencryptedAuthentication = $true + } + $Response = Invoke-WebRequest @params + $result = $response.Content | ConvertFrom-Json + + $result.Headers.Authorization | Should Match '^Negotiate ' + } } BeforeEach { @@ -2233,6 +2306,10 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { $credential = [pscredential]::new("testuser",$token) $httpUri = Get-WebListenerUrl -Test 'Get' $httpsUri = Get-WebListenerUrl -Test 'Get' -Https + $httpBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' + $httpsBasicUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' -Https + $httpNegotiateUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Negotiate' + $httpsNegotiateUri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Negotiate' -Https $testCases = @( @{Authentication = "bearer"} @{Authentication = "OAuth"} @@ -2330,6 +2407,71 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { $result.Headers.Authorization | Should BeExactly "Bearer testpassword" } + + It "Verifies Invoke-RestMethod Negotiated -Credential over HTTPS" { + $params = @{ + Uri = $httpsBasicUri + Credential = $credential + SkipCertificateCheck = $true + } + $result = Invoke-RestMethod @params + + $result.Headers.Authorization | Should BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" + } + + It "Verifies Invoke-RestMethod Negotiated -Credential Requires HTTPS" { + $params = @{ + Uri = $httpBasicUri + Credential = $credential + ErrorAction = 'Stop' + } + { Invoke-RestMethod @params } | ShouldBeErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" + } + + It "Verifies Invoke-RestMethod Negotiated -Credential Can use HTTP with -AllowUnencryptedAuthentication" { + $params = @{ + Uri = $httpBasicUri + Credential = $credential + AllowUnencryptedAuthentication = $true + } + $result = Invoke-RestMethod @params + + $result.Headers.Authorization | Should BeExactly "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" + } + + # UseDefaultCredentials is only reliably testable on Windows + It "Verifies Invoke-RestMethod Negotiated -UseDefaultCredentials over HTTPS" -Skip:$(!$IsWindows) { + $params = @{ + Uri = $httpsNegotiateUri + Credential = $credential + SkipCertificateCheck = $true + } + $result = Invoke-RestMethod @params + + $result.Headers.Authorization | Should Match '^Negotiate ' + } + + # The error condition can at least be tested on all platforms. + It "Verifies Invoke-RestMethod Negotiated -UseDefaultCredentials Requires HTTPS" { + $params = @{ + Uri = $httpNegotiateUri + UseDefaultCredentials = $true + ErrorAction = 'Stop' + } + { Invoke-RestMethod @params } | ShouldBeErrorId "WebCmdletAllowUnencryptedAuthenticationRequiredException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand" + } + + # UseDefaultCredentials is only reliably testable on Windows + It "Verifies Invoke-RestMethod Negotiated -UseDefaultCredentials Can use HTTP with -AllowUnencryptedAuthentication" -Skip:$(!$IsWindows) { + $params = @{ + Uri = $httpNegotiateUri + UseDefaultCredentials = $true + AllowUnencryptedAuthentication = $true + } + $result = Invoke-RestMethod @params + + $result.Headers.Authorization | Should Match '^Negotiate ' + } } BeforeEach { diff --git a/test/tools/Modules/WebListener/WebListener.psm1 b/test/tools/Modules/WebListener/WebListener.psm1 index a3125d07c6b..3aa83c7a81e 100644 --- a/test/tools/Modules/WebListener/WebListener.psm1 +++ b/test/tools/Modules/WebListener/WebListener.psm1 @@ -113,6 +113,7 @@ function Get-WebListenerUrl { param ( [switch]$Https, [ValidateSet( + 'Auth', 'Cert', 'Compression', 'Delay', diff --git a/test/tools/WebListener/Controllers/AuthController.cs b/test/tools/WebListener/Controllers/AuthController.cs new file mode 100644 index 00000000000..c75eff0b7e7 --- /dev/null +++ b/test/tools/WebListener/Controllers/AuthController.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Primitives; +using mvc.Models; + +namespace mvc.Controllers +{ + public class AuthController : Controller + { + public JsonResult Basic() + { + StringValues authorization; + if (Request.Headers.TryGetValue("Authorization", out authorization)) + { + var getController = new GetController(); + getController.ControllerContext = this.ControllerContext; + return getController.Index(); + } + else + { + Response.Headers.Add("WWW-Authenticate","Basic realm=\"WebListener\""); + Response.StatusCode = 401; + return Json("401 Unauthorized"); + } + } + + public JsonResult Negotiate() + { + StringValues authorization; + if (Request.Headers.TryGetValue("Authorization", out authorization)) + { + var getController = new GetController(); + getController.ControllerContext = this.ControllerContext; + return getController.Index(); + } + else + { + Response.Headers.Add("WWW-Authenticate","Negotiate"); + Response.StatusCode = 401; + return Json("401 Unauthorized"); + } + } + + public JsonResult Ntlm() + { + StringValues authorization; + if (Request.Headers.TryGetValue("Authorization", out authorization)) + { + var getController = new GetController(); + getController.ControllerContext = this.ControllerContext; + return getController.Index(); + } + else + { + Response.Headers.Add("WWW-Authenticate","NTLM"); + Response.StatusCode = 401; + return Json("401 Unauthorized"); + } + } + + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + } +} diff --git a/test/tools/WebListener/README.md b/test/tools/WebListener/README.md index 04dbd20ccb5..5aca1cc4def 100644 --- a/test/tools/WebListener/README.md +++ b/test/tools/WebListener/README.md @@ -34,6 +34,76 @@ $Listener = Start-WebListener -HttpPort 8083 -HttpsPort 8084 Returns a static HTML page containing links and descriptions of the available tests in WebListener. This can be used as a default or general test where no specific test functionality or return data is required. +## /Auth/Basic/ + +Provides a mock Basic authentication challenge. If a basic authorization header is sent, then the same results as /Get/ are returned. + +```powershell +$credential = Get-Credential +$uri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Basic' -Https +Invoke-RestMethod -Uri $uri -Credential $credential -SkipCertificateCheck +``` + +```json +{ + "headers":{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0", + "Connection": "Keep-Alive", + "Authorization": "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk", + "Host": "localhost:8084" + }, + "origin": "127.0.0.1", + "args": {}, + "url": "https://localhost:8084/Auth/Basic" +} +``` + +## /Auth/Negotiate/ + +Provides a mock Negotiate authentication challenge. If a basic authorization header is sent, then the same results as /Get/ are returned. + +```powershell +$uri = Get-WebListenerUrl -Test 'Auth' -TestValue 'Negotiate' -Https +Invoke-RestMethod -Uri $uri -UseDefaultCredential -SkipCertificateCheck +``` + +```json +{ + "headers":{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0", + "Connection": "Keep-Alive", + "Authorization": "Negotiate jjaguasgtisi7tiqkagasjjajvs", + "Host": "localhost:8084" + }, + "origin": "127.0.0.1", + "args": {}, + "url": "https://localhost:8084/Auth/Negotiate" +} +``` + +## /Auth/NTLM/ + +Provides a mock NTLM authentication challenge. If a basic authorization header is sent, then the same results as /Get/ are returned. + +```powershell +$uri = Get-WebListenerUrl -Test 'Auth' -TestValue 'NTLM' -Https +Invoke-RestMethod -Uri $uri -UseDefaultCredential -SkipCertificateCheck +``` + +```json +{ + "headers":{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0", + "Connection": "Keep-Alive", + "Authorization": "NTLM jjaguasgtisi7tiqkagasjjajvs", + "Host": "localhost:8084" + }, + "origin": "127.0.0.1", + "args": {}, + "url": "https://localhost:8084/Auth/NTLM" +} +``` + ## /Cert/ Returns a JSON object containing the details of the Client Certificate if one is provided in the request. diff --git a/test/tools/WebListener/Views/Home/Index.cshtml b/test/tools/WebListener/Views/Home/Index.cshtml index 6c88ce5e0a3..13699a36b33 100644 --- a/test/tools/WebListener/Views/Home/Index.cshtml +++ b/test/tools/WebListener/Views/Home/Index.cshtml @@ -1,6 +1,9 @@ 

Available Tests