Strip authorization header on redirects with web cmdlets (#3885)

Invoke-WebRequest and Invoke-RestMethod cmdlets will now strip authorization header on redirect unless the new parameter `-PreserveAuthorizationOnRedirect` is specified.

The FullCLR implementation uses WebRequest to perform the request which silently strips the Authorization header when a redirect occurs.

The CoreCLR implementation uses HttpClient to perform the request which does not strip the authorization header. The change explicitly handles the initial redirect, removes the authorization header and submits the request to location in the response.

Fixes #2227
This commit is contained in:
Dan Travison
2017-06-15 15:52:08 -07:00
committed by Jason Shirk
parent 26a44abcec
commit 039ed6764d
3 changed files with 372 additions and 22 deletions
@@ -143,6 +143,70 @@ function GetTestData
return $body
}
function ExecuteRedirectRequest
{
param (
[Parameter(Mandatory)]
[string]
$uri,
[ValidateSet('Invoke-WebRequest', 'Invoke-RestMethod')]
[string] $Cmdlet = 'Invoke-WebRequest',
[ValidateSet('POST', 'GET')]
[string] $Method = 'GET',
[switch] $PreserveAuthorizationOnRedirect
)
$result = [PSObject]@{Output = $null; Error = $null; Content = $null}
try
{
$headers = @{"Authorization" = "test"}
if ($Cmdlet -eq 'Invoke-WebRequest')
{
$result.Output = Invoke-WebRequest -Uri $uri -TimeoutSec 5 -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -Method $Method
$result.Content = $result.Output.Content | ConvertFrom-Json
}
else
{
$result.Output = Invoke-RestMethod -Uri $uri -TimeoutSec 5 -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -Method $Method
# NOTE: $result.Output should already be a PSObject (Invoke-RestMethod converts the returned json automatically)
# so simply reference $result.Output
$result.Content = $result.Output
}
}
catch
{
$result.Error = $_
}
return $result
}
<#
Defines the list of redirect codes to test as well as the
expected Method when the redirection is handled.
See https://msdn.microsoft.com/en-us/library/windows/apps/system.net.httpstatuscode(v=vs.105).aspx
for additonal details.
#>
$redirectTests = @(
@{redirectType = 'MultipleChoices'; redirectedMethod='POST'}
@{redirectType = 'Ambiguous'; redirectedMethod='POST'} # Synonym for MultipleChoices
@{redirectType = 'Moved'; redirectedMethod='GET'}
@{redirectType = 'MovedPermanently'; redirectedMethod='GET'} # Synonym for Moved
@{redirectType = 'Found'; redirectedMethod='GET'}
@{redirectType = 'Redirect'; redirectedMethod='GET'} # Synonym for Found
@{redirectType = 'redirectMethod'; redirectedMethod='GET'}
@{redirectType = 'SeeOther'; redirectedMethod='GET'} # Synonym for RedirectMethod
@{redirectType = 'TemporaryRedirect'; redirectedMethod='GET'}
@{redirectType = 'RedirectKeepVerb'; redirectedMethod='GET'} # Synonym for TemporaryRedirect
)
Describe "Invoke-WebRequest tests" -Tags "Feature" {
BeforeAll {
@@ -223,7 +287,7 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" {
$jsonContent.headers.Host | Should Match "httpbin.org"
$jsonContent.headers.'User-Agent' | Should Match "WindowsPowerShell"
}
It "Validate Invoke-WebRequest error for -MaximumRedirection" {
$command = "Invoke-WebRequest -Uri 'http://httpbin.org/redirect/3' -MaximumRedirection 2 -TimeoutSec 5"
@@ -231,7 +295,7 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" {
$result = ExecuteWebCommand -command $command
$result.Error.FullyQualifiedErrorId | Should Be "WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand"
}
It "Invoke-WebRequest supports request that returns page containing UTF-8 data." {
$command = "Invoke-WebRequest -Uri http://httpbin.org/encoding/utf8 -TimeoutSec 5"
@@ -545,6 +609,59 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" {
$result.Output.RelationLink["last"] | Should BeExactly "http://localhost:8080/PowerShell?test=linkheader&maxlinks=3&linknumber=3"
}
#region Redirect tests
It "Validates Invoke-WebRequest with -PreserveAuthorizationOnRedirect preserves the authorization header on redirect" -TestCases $redirectTests {
param($redirectType, $redirectedMethod)
$response = ExecuteRedirectRequest -Uri "http://localhost:8080/PowerShell?test=redirect&type=$redirectType" -PreserveAuthorizationOnRedirect
$response.Error | Should BeNullOrEmpty
# ensure Authorization header has been preserved.
$response.Content.Headers -contains "Authorization" | Should Be $true
}
It "Validates Invoke-WebRequest preserves the authorization header on multiple redirects." -TestCases $redirectTests {
param($redirectType)
$response = ExecuteRedirectRequest -Uri "http://localhost:8080/PowerShell?test=redirect&type=$redirectType&multiredirect=true" -PreserveAuthorizationOnRedirect
$response.Error | Should BeNullOrEmpty
# ensure Authorization header was stripped
$response.Content.Headers -contains "Authorization" | Should Be $true
}
It "Validates Invoke-WebRequest strips the authorization header on various redirects" -TestCases $redirectTests {
param($redirectType)
$response = ExecuteRedirectRequest -Uri "http://localhost:8080/PowerShell?test=redirect&type=$redirectType"
$response.Error | Should BeNullOrEmpty
# ensure user-agent is present (i.e., no false positives )
$response.Content.Headers -contains "User-Agent" | Should Be $true
# ensure Authorization header has been removed.
$response.Content.Headers -contains "Authorization" | Should Be $false
}
# NOTE: Only testing redirection of POST -> GET for unique underlying values of HttpStatusCode.
# Some names overlap in underlying value.
It "Validates Invoke-WebRequest strips the authorization header redirects and switches from POST to GET when it handles the redirect" -TestCases $redirectTests {
param($redirectType, $redirectedMethod)
$response = ExecuteRedirectRequest -Uri "http://localhost:8080/PowerShell?test=redirect&type=$redirectType" -Method 'POST'
$response.Error | Should BeNullOrEmpty
# ensure user-agent is present (i.e., no false positives )
$response.Content.Headers -contains "User-Agent" | Should Be $true
# ensure Authorization header has been removed.
$response.Content.Headers -contains "Authorization" | Should Be $false
# ensure POST was changed to GET for selected redirections and remains as POST for others.
$response.Content.HttpMethod | Should Be $redirectedMethod
}
#endregion Redirect tests
BeforeEach {
if ($env:http_proxy) {
$savedHttpProxy = $env:http_proxy
@@ -955,6 +1072,58 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" {
$result.Output.output | Should BeExactly 1
}
#region Redirect tests
It "Validates Invoke-RestMethod with -PreserveAuthorizationOnRedirect preserves the authorization header on redirect" -TestCases $redirectTests {
param($redirectType, $redirectedMethod)
$response = ExecuteRedirectRequest -Cmdlet 'Invoke-RestMethod' -Uri "http://localhost:8081/PowerShell?test=redirect&type=$redirectType" -PreserveAuthorizationOnRedirect
$response.Error | Should BeNullOrEmpty
# ensure Authorization header has been preserved.
$response.Content.Headers -contains "Authorization" | Should Be $true
}
It "Validates Invoke-RestMethod preserves the authorization header on multiple redirects." -TestCases $redirectTests {
param($redirectType)
$response = ExecuteRedirectRequest -Cmdlet 'Invoke-RestMethod' -Uri "http://localhost:8081/PowerShell?test=redirect&type=$redirectType&multiredirect=true" -PreserveAuthorizationOnRedirect
$response.Error | Should BeNullOrEmpty
# ensure Authorization header was stripped
$response.Content.Headers -contains "Authorization" | Should Be $true
}
It "Validates Invoke-RestMethod strips the authorization header on various redirects" -TestCases $redirectTests {
param($redirectType)
$response = ExecuteRedirectRequest -Cmdlet 'Invoke-RestMethod' -Uri "http://localhost:8081/PowerShell?test=redirect&type=$redirectType"
$response.Error | Should BeNullOrEmpty
# ensure user-agent is present (i.e., no false positives )
$response.Output.Headers -contains "User-Agent" | Should Be $true
# ensure Authorization header has been removed.
$response.Content.Headers -contains "Authorization" | Should Be $false
}
# NOTE: Only testing redirection of POST -> GET for unique underlying values of HttpStatusCode.
# Some names overlap in underlying value.
It "Validates Invoke-RestMethod strips the authorization header redirects and switches from POST to GET when it handles the redirect" -TestCases $redirectTests {
param($redirectType, $redirectedMethod)
$response = ExecuteRedirectRequest -Cmdlet 'Invoke-RestMethod' -Uri "http://localhost:8081/PowerShell?test=redirect&type=$redirectType" -Method 'POST'
$response.Error | Should BeNullOrEmpty
# ensure user-agent is present (i.e., no false positives )
$response.Content.Headers -contains "User-Agent" | Should Be $true
# ensure Authorization header has been removed.
$response.Content.Headers -contains "Authorization" | Should Be $false
# ensure POST was changed to GET for selected redirections and remains as POST for others.
$response.Content.HttpMethod | Should Be $redirectedMethod
}
#endregion Redirect tests
BeforeEach {
if ($env:http_proxy) {
$savedHttpProxy = $env:http_proxy
@@ -46,6 +46,15 @@ Function Start-HTTPListener {
[scriptblock]$script = {
param ($Port)
$script:supportedRedirects = @{
[System.Net.HttpStatusCode]::Ok = 1; # No redirect
[System.Net.HttpStatusCode]::Found = 1;
[System.Net.HttpStatusCode]::MultipleChoices = 1;
[System.Net.HttpStatusCode]::Moved = 1;
[System.Net.HttpStatusCode]::SeeOther = 1;
[System.Net.HttpStatusCode]::TemporaryRedirect = 1;
}
# HttpListener.QueryString is not being populated, need to follow-up with CoreFx, workaround is to parse it ourselves
Function ParseQueryString([string]$url)
{
@@ -92,6 +101,11 @@ Function Start-HTTPListener {
$test = $queryItems["test"]
Write-Verbose "Testing: $test"
foreach ($key in $request.Headers.Keys)
{
Write-Verbose -Message "Found Header: $key, $($request.Headers[$key])"
}
# the status code to return for the response
$statusCode = [System.Net.HttpStatusCode]::OK
# this is the body of the response, return json/xml as appropriate
@@ -120,21 +134,75 @@ Function Start-HTTPListener {
$contentType = $queryItems["contenttype"]
$output = $queryItems["output"]
}
<#
This test provides support for multiple redirection types as well as a custom
multi-hop redirection to handle Authorization stripping logic.
The following redirection types are supported:
MultipleChoices (300), Moved (301), Found (302), SeeOther (303), TemporaryRedirect (307)
The original URL should indicate the type of redirection.
For example: The following indicates that a 302 redirection (found) should be used.
?test=redirectex&type=Found
WebRequest cmdlet tests also use a special option called multiredirect. This produces two redirects
where the second
Example: test=redirectex&type=Moved&multiredirect=true
See also https://msdn.microsoft.com/en-us/library/system.net.httpstatuscode(v=vs.110).aspx
#>
"redirect"
{
$redirect = $queryItems["redirect"]
if ($redirect -eq $null)
$redirectedUrl = [string]::Empty
$redirectType = $queryItems["type"]
$multiredirect = $queryItems["multiredirect"]
if ($redirectType -eq $null)
{
$statusCode = [System.Net.HttpStatusCode]::Found
$redirectedUrl = "${Url}?test=redirect&redirect=false"
Write-Verbose "$redirectedUrl"
$outputHeader.Add("Location",$redirectedUrl)
Write-Verbose "Redirecting to $($outputHeader.Location)"
# End of redirection
$redirectType = 'Ok'
}
else
[System.Net.HttpStatusCode] $type = [System.Net.HttpStatusCode]::Found
[bool] $isValid = [System.Enum]::TryParse($redirectType, $true, [ref] $type)
if ($isValid -eq $false -or $script:supportedRedirects.ContainsKey($type) -eq $false)
{
$output = $request | ConvertTo-Json
Write-Verbose -Message "Invalid request type: $type"
$statusCode = [System.Net.HttpStatusCode]::BadRequest
$output = "Invalid Redirect Type: $type"
}
elseif ($type -eq [System.Net.HttpStatusCode]::Ok)
{
# no redirection
Write-Verbose -Message "No redirection"
$output = $request | ConvertTo-Json -Depth 6
}
elseif ($multiredirect -eq $null)
{
Write-Verbose -Message "Standard redirection"
$redirectedUrl = "${Url}?test=redirect&type=Ok"
}
elseif ($multiredirect -eq $true)
{
Write-Verbose -Message "Redirect 1 of 2"
$redirectedUrl = "${Url}?test=redirect&type=$type&multiredirect=false"
}
elseif ($multiredirect -eq $false)
{
Write-Verbose -Message "Redirect 2 of 2"
$redirectedUrl = "${Url}?test=redirect&type=$type"
}
if ($isValid)
{
$statusCode = $type
if (-not [string]::IsNullOrEmpty($redirectedUrl))
{
$outputHeader.Add("Location",$redirectedUrl)
Write-Verbose -Message "Redirecting to $($outputHeader.Location)"
}
}
}
"linkheader"
{
@@ -189,11 +257,19 @@ Function Start-HTTPListener {
}
$response = $context.Response
if ($contentType -ne $null)
if ($outputHeader.ContainsKey('Content-Type') -eq $false)
{
Write-Verbose "Setting ContentType to $contentType"
if ([string]::IsNullOrEmpty($contentType))
{
$contentType = 'application/json'
}
$outputHeader.Add('Content-Type', $contentType)
$response.ContentType = $contentType
Write-Verbose -Message "Setting ContentType to $contentType"
}
if ($statusCode -ne $null)
{
$response.StatusCode = $statusCode
@@ -203,6 +279,7 @@ Function Start-HTTPListener {
{
$response.Headers.Add($header, $outputHeader[$header])
}
if ($output -ne $null)
{
$buffer = [System.Text.Encoding]::UTF8.GetBytes($output)