.NET Core安全性增强导致HttpClientHandler的AllowAutoRedirect属性无效问题分析

By | 2022年2月22日

最近在工作中发现一个莫名其妙的Bug,考察下面的代码:

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 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 to true.

(参考: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:

在.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);

 

(总访问量:241;当日访问量:1)

发表回复

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据