8000 Strip authorization header on redirects with web cmdlets (#3885) · PowerShell/PowerShell@039ed67 · GitHub
[go: up one dir, main page]

Skip to content

Commit 039ed67

Browse files
dantraMSFTlzybkr
authored andcommitted
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
1 parent 26a44ab commit 039ed67

File tree

3 files changed

+372
-22
lines changed

3 files changed

+372
-22
lines changed

src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebRequestPSCmdlet.CoreClr.cs

Lines changed: 113 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ namespace Microsoft.PowerShell.Commands
2424
{
2525
/// <summary>
2626
/// Exception class for webcmdlets to enable returning HTTP error response
27-
/// </summary>
27+
/// </summary>
2828
public sealed class HttpResponseException : HttpRequestException
2929
{
3030
/// <summary>
@@ -48,6 +48,22 @@ public HttpResponseException (string message, HttpResponseMessage response) : ba
4848
/// </summary>
4949
public abstract partial class WebRequestPSCmdlet : PSCmdlet
5050
{
51+
52+
/// <summary>
53+
/// gets or sets the PreserveAuthorizationOnRedirect property
54+
/// </summary>
55+
/// <remarks>
56+
/// This property overrides compatibility with web requests on Windows.
57+
/// On FullCLR (WebRequest), authorization headers are stripped during redirect.
58+
/// CoreCLR (HTTPClient) does not have this behavior so web requests that work on
59+
/// PowerShell/FullCLR can fail with PowerShell/CoreCLR. To provide compatibility,
60+
/// we'll detect requests with an Authorization header and automatically strip
61+
/// the header when the first redirect occurs. This switch turns off this logic for
62+
/// edge cases where the authorization header needs to be preserved across redirects.
63+
/// </remarks>
64+
[Parameter]
65+
public virtual SwitchParameter PreserveAuthorizationOnRedirect { get; set; }
66+
8000 5167
#region Abstract Methods
5268

5369
/// <summary>
@@ -111,7 +127,9 @@ private HttpMethod GetHttpMethod(WebRequestMethod method)
111127

112128
#region Virtual Methods
113129

114-
internal virtual HttpClient GetHttpClient()
130+
// NOTE: Only pass true for handleRedirect if the original request has an authorization header
131+
// and PreserveAuthorizationOnRedirect is NOT set.
132+
internal virtual HttpClient GetHttpClient(bool handleRedirect)
115133
{
116134
// By default the HttpClientHandler will automatically decompress GZip and Deflate content
117135
HttpClientHandler handler = new HttpClientHandler();
@@ -150,7 +168,12 @@ internal virtual HttpClient GetHttpClient()
150168
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
151169
}
152170

153-
if (WebSession.MaximumRedirection > -1)
171+
// This indicates GetResponse will handle redirects.
172+
if (handleRedirect)
173+
{
174+
handler.AllowAutoRedirect = false;
175+
}
176+
else if (WebSession.MaximumRedirection > -1)
154177
{
155178
if (WebSession.MaximumRedirection == 0)
156179
{
@@ -178,7 +201,7 @@ internal virtual HttpClient GetHttpClient()
178201
return httpClient;
179202
}
180203

181-
internal virtual HttpRequestMessage GetRequest(Uri uri)
204+
internal virtual HttpRequestMessage GetRequest(Uri uri, bool stripAuthorization)
182205
{
183206
Uri requestUri = PrepareUri(uri);
184207
HttpMethod httpMethod = null;
@@ -217,6 +240,13 @@ internal virtual HttpRequestMessage GetRequest(Uri uri)
217240
}
218241
else
219242
{
243+
if (stripAuthorization
244+< 67E6 /span>
&&
245+
String.Equals(entry.Key, HttpKnownHeaderNames.Authorization.ToString(), StringComparison.OrdinalIgnoreCase)
246+
)
247+
{
248+
continue;
249+
}
220250
request.Headers.Add(entry.Key, entry.Value);
221251
}
222252
}
@@ -367,13 +397,77 @@ internal virtual void FillRequestStream(HttpRequestMessage request)
367397
}
368398
}
369399

370-
internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request)
400+
// Returns true if the status code is one of the supported redirection codes.
401+
static bool IsRedirectCode(HttpStatusCode code)
402+
{
403+
int intCode = (int) A3E2 code;
404+
return
405+
(
406+
(intCode >= 300 && intCode < 304)
407+
||
408+
intCode == 307
409+
);
410+
}
411+
412+
// Returns true if the status code is a redirection code and the action requires switching from POST to GET on redirection.
413+
// NOTE: Some of these status codes map to the same underlying value but spelling them out for completeness.
414+
static bool IsRedirectToGet(HttpStatusCode code)
415+
{
416+
return
417+
(
418+
code == HttpStatusCode.Found
419+
||
420+
code == HttpStatusCode.Moved
421+
||
422+
code == HttpStatusCode.Redirect
423+
||
424+
code == HttpStatusCode.RedirectMethod
425+
||
426+
code == HttpStatusCode.TemporaryRedirect
427+
||
428+
code == HttpStatusCode.RedirectKeepVerb
429+
||
430+
code == HttpStatusCode.SeeOther
431+
);
432+
}
433+
434+
internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request, bool stripAuthorization)
371435
{
372436
if (client == null) { throw new ArgumentNullException("client"); }
373437
if (request == null) { throw new ArgumentNullException("request"); }
374438

375439
_cancelToken = new CancellationTokenSource();
376-
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();
440+
HttpResponseMessage response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();
441+
442+
if (stripAuthorization && IsRedirectCode(response.StatusCode))
443+
{
444+
_cancelToken.Cancel();
445+
_cancelToken = null;
446+
447+
// if explicit count was provided, reduce it for this redirection.
448+
if (WebSession.MaximumRedirection > 0)
449+
{
450+
WebSession.MaximumRedirection--;
451+
}
452+
// For selected redirects that used POST, GET must be used with the
453+
// redirected Location.
454+
// Since GET is the default; POST only occurs when -Method POST is used.
455+
if (Method == WebRequestMethod.Post && IsRedirectToGet(response.StatusCode))
456+
{
457+
// See https://msdn.microsoft.com/en-us/library/system.net.httpstatuscode(v=vs.110).aspx
458+
Method = WebRequestMethod.Get;
459+
}
460+
461+
// recreate the HttpClient with redirection enabled since the first call suppressed redirection
462+
using (client = GetHttpClient(false))
463+
using (HttpRequestMessage redirectRequest = GetRequest(response.Headers.Location, stripAuthorization:true))
464+
{
465+
FillRequestStream(redirectRequest);
466+
_cancelToken = new CancellationTokenSource();
467+
response = client.SendAsync(redirectRequest, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();
468+
}
469+
}
470+
return response;
377471
}
378472

379473
internal virtual void UpdateSession(HttpResponseMessage response)
@@ -396,7 +490,17 @@ protected override void ProcessRecord()
396490
ValidateParameters();
397491
PrepareSession();
398492

399-
using (HttpClient client = GetHttpClient())
493+
// if the request contains an authorization header and PreserveAuthorizationOnRedirect is not set,
494+
// it needs to be stripped on the first redirect.
495+
bool stripAuthorization = null != WebSession
496+
&&
497+
null != WebSession.Headers
498+
&&
499+
!PreserveAuthorizationOnRedirect.IsPresent
500+
&&
501+
WebSession.Headers.ContainsKey(HttpKnownHeaderNames.Authorization.ToString());
502+
503+
using (HttpClient client = GetHttpClient(stripAuthorization))
400504
{
401505
int followedRelLink = 0;
402506
Uri uri = Uri;
@@ -410,7 +514,7 @@ protected override void ProcessRecord()
410514
WriteVerbose(linkVerboseMsg);
411515
}
412516

413-
using (HttpRequestMessage request = GetRequest(uri))
517+
using (HttpRequestMessage request = GetRequest(uri, stripAuthorization:false))
414518
{
415519
FillRequestStream(request);
416520
try
@@ -426,7 +530,7 @@ protected override void ProcessRecord()
426530
requestContentLength);
427531
WriteVerbose(reqVerboseMsg);
428532

429-
HttpResponseMessage response = GetResponse(client, request);
533+
HttpResponseMessage response = GetResponse(client, request, stripAuthorization);
430534

431535
string contentType = ContentHelper.GetContentType(response);
432536
string respVerboseMsg = string.Format(CultureInfo.CurrentCulture,

0 commit comments

Comments
 (0)
0