.NET Core安全性增强导致HttpClientHandler的AllowAutoRedirect属性无效问题分析
最近在工作中发现一个莫名其妙的Bug,考察下面的代码:
在.NET Runtime的Github repo上,也有不少人提议,在HttpClientHandler上新加一个属性,用于启用从HTTPS到HTTP的重定向,或者提供另外的API以简化对于302返回代码的处理。然而,最终该帖子以“Won't Fix”的方式关闭了。参考:https://github.com/dotnet/runtime/issues/28039。
解决这个问题的方法就是自己写代码完成重定向。代码有很多种写法,下面就是一种比较简单的做法:
var baseUrl = "https://test.example.com/";
var loginUrl = $"{baseUrl}sso-auth/login";
var userInfoUrl = $"{baseUrl}sso-auth/user-info";
var cookieAndCsrf = await GetNewSessionCookiesAndCSRFAsync(loginUrl).ConfigureAwait(false);
var username = "username";
var password = "password";
var postFormContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("username", username),
new KeyValuePair<string, string>("password", password),
new KeyValuePair<string, string>("_csrf", cookieAndCsrf.Item2)
});
var cookieContainer = new CookieContainer();
using var httpClientHandler = new HttpClientHandler() { CookieContainer = cookieContainer };
using var httpClient = new HttpClient(httpClientHandler);
var request = new HttpRequestMessage(HttpMethod.Post, loginUrl)
{
Content = postFormContent
};
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
cookieAndCsrf.Item1.ToList().ForEach(k => cookieContainer.Add(new Uri(baseUrl), k));
var response = await httpClient.SendAsync(request).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
这段代码的主要目的是为了从服务端的login的界面,通过以表单的方式传入用户名和密码,然后获取认证信息。在以前版本的产品中,这部分代码可以正常运行,然而,最近发现,当获取到response之后,HTTP的返回状态为302 Found,已经不再是200 OK了。
考虑到代码一直没有动过,唯一动过的地方就是升级了.NET,从原来的仅支持Windows的.NET Framework 4.7升级到了.NET 6,目的是为了能够让这个库跨平台使用。考虑到这一层面,我又新建了一个控制台应用程序,使其仅在.NET Framework 4.7下运行,然后重新调用上面的代码来观察response的状态码,发现确实为200 OK。至此,基本可以确定就是升级.NET版本所致。
经过一番查找和求证,发现.NET 6(其实从.NET Core 1.0开始)对于HttpClientHandler的AllowAutoRedirect属性的定义有一定的变化:当POST的response从https重定向到http时,在老版本的.NET中,如果设置AllowAutoRedirect为true(默认值是true),那么.NET Framework会自动帮你完成重定向,并得到最终一轮重定向的返回结果;而在.NET Core下,出于安全性考虑,如果是https重定向到http时,.NET将不再为你代劳,而直接返回3xx的代码,即使你的HttpClientHandler的AllowAutoRedirect被设置为true。微软官方文档也说明了这一点:
With AllowAutoRedirect set to(参考:https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler.allowautoredirect?view=net-6.0#remarks) 回到上面的代码,可以看到,response.Location是一个http的地址,因此,在.NET 6中,返回的状态码就是302,而不是200:true, the .NET Framework will follow redirections even when being redirected to an HTTP URI from an HTTPS URI. .NET Core versions 1.0, 1.1 and 2.0 will not follow a redirection from HTTPS to HTTP even if AllowAutoRedirect is set totrue.
在.NET Runtime的Github repo上,也有不少人提议,在HttpClientHandler上新加一个属性,用于启用从HTTPS到HTTP的重定向,或者提供另外的API以简化对于302返回代码的处理。然而,最终该帖子以“Won't Fix”的方式关闭了。参考:https://github.com/dotnet/runtime/issues/28039。
解决这个问题的方法就是自己写代码完成重定向。代码有很多种写法,下面就是一种比较简单的做法:
private const int MaximumAutomaticRedirects = 50;
private static readonly HttpStatusCode[] RedirectStatusCodes = new[]
{
HttpStatusCode.Moved,
HttpStatusCode.MovedPermanently,
HttpStatusCode.Found,
HttpStatusCode.Redirect,
HttpStatusCode.RedirectMethod,
HttpStatusCode.SeeOther,
HttpStatusCode.RedirectKeepVerb,
HttpStatusCode.TemporaryRedirect
};
private static async Task<HttpResponseMessage> SendWithRedirectAsync(HttpClient httpClient, HttpRequestMessage requestMessage)
{
var redirectCount = 0;
var response = await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
while (RedirectStatusCodes.Any(c => response.StatusCode == c))
{
var nextRequestUri = response.Headers.Location;
if (nextRequestUri == null)
{
throw new AuthenticationException("The response indicates a redirect, but the redirect URI is not specified in the Location header");
}
if (++redirectCount == MaximumAutomaticRedirects)
{
throw new AuthenticationException($"Too many redirects. Maximum number of redirects is set to {MaximumAutomaticRedirects}.");
}
var nextRequest = new HttpRequestMessage(HttpMethod.Get, nextRequestUri);
response = await httpClient.SendAsync(nextRequest).ConfigureAwait(false);
}
return response;
}
最后,只需要将第一段代码中的
var response = await httpClient.SendAsync(request).ConfigureAwait(false);改为如下即可:
var response = await SendWithRedirectAsync(httpClient, request).ConfigureAwait(false);

浙公网安备 33010602011771号