防止开放重定向攻击 (C#)

作者 :乔恩·加洛韦

本教程介绍如何在 ASP.NET MVC 应用程序中防止打开重定向攻击。 本教程讨论在 ASP.NET MVC 3 中 AccountController 中所做的更改,并演示如何在现有 ASP.NET MVC 1.0 和 2 应用程序中应用这些更改。

什么是开放重定向攻击?

重定向到通过请求指定的 URL 的任何 Web 应用程序(例如查询字符串或表单数据)都可能会被篡改,以将用户重定向到外部恶意 URL。 这种篡改称为开放式重定向攻击。

每当应用程序逻辑重定向到指定的 URL 时,你必须验证重定向 URL 是否未被篡改。 ASP.NET MVC 1.0 和 ASP.NET MVC 2 的默认 AccountController 中使用的登录功能容易受到开放重定向攻击。 幸运的是,可以轻松更新现有应用程序以使用 ASP.NET MVC 3 预览版中的更正。

为了了解漏洞,让我们看看登录重定向在默认 ASP.NET MVC 2 Web 应用程序项目中的工作原理。 在此应用程序中,尝试访问具有 [Authorize] 属性的控制器操作会将未经授权的用户重定向到 /Account/LogOn 视图。 此重定向到 /Account/LogOn 将包含 returnUrl 查询字符串参数,以便用户可以在成功登录后返回到最初请求的 URL。

在下面的屏幕截图中,可以看到未登录时尝试访问 /Account/ChangePassword 视图会导致重定向到 /Account/LogOn?ReturnUrl=%2fAccount%2fChangePassword%2f。

显示“我的 M V C 应用程序登录”页的屏幕截图。标题栏突出显示。

图01:具有开放重定向的登录页

由于 ReturnUrl 查询字符串参数未得到验证,因此攻击者可以对其进行修改,以将任何 URL 地址注入参数,以执行开放式重定向攻击。 为了演示这一点,我们可以将 ReturnUrl 参数修改为 https://bing.com,因此生成的登录 URL 将为 /Account/LogOn?ReturnUrl=https://www.bing.com/。 成功登录到站点后,我们将重定向到 https://bing.com。 由于未验证此重定向,因此可能会指向试图欺骗用户的恶意站点。

更复杂的开放重定向攻击

开放重定向攻击尤其危险,因为攻击者知道我们正尝试登录到特定网站,这使得我们容易受到 网络钓鱼攻击。 例如,攻击者可能会向网站用户发送恶意电子邮件,以尝试捕获其密码。 让我们看看这将如何在 NerdDinner 网站上工作。 (请注意,实时 NerdDinner 站点已更新,以防止开放重定向攻击。

首先,攻击者向我们发送一个指向 NerdDinner 上的登录页的链接,其中包括重定向到其伪造页面:

http://nerddinner.com/Account/LogOn?returnUrl=http://nerddiner.com/Account/LogOn

请注意,返回 URL 指向 nerddiner.com,该 URL 缺少单词晚餐中的“n”。 在此示例中,这是攻击者控制的域。 当我们访问上述链接时,我们会访问合法 NerdDinner.com 登录页。

显示 Nerd Dinner dot com 主页的屏幕截图。标题栏突出显示并填充指向 Nerd Diner dot com 的 U R L。

图 02:具有开放重定向的 NerdDinner 登录页面

正确登录时,ASP.NET MVC AccountController 的 LogOn 操作会将我们重定向到 returnUrl 查询字符串参数中指定的 URL。 在本例中,攻击者输入的 URL 是 http://nerddiner.com/Account/LogOn。 除非我们非常警惕,否则我们很可能不会注意到这一点,特别是因为攻击者一直小心翼翼地确保其伪造的页面看起来与合法登录页完全相同。 此登录页包含一条错误消息,请求我们再次登录。 我们真是粗心,可能是把密码输错了。

显示伪造的 Nerd Dinner Log On 页面的屏幕截图,提示用户重新输入其凭据。标题栏中伪造的 U R L 突出显示。

图 03:伪造的 NerdDinner 登录屏幕

重新键入用户名和密码时,伪造的登录页将保存信息,并将我们发送回合法 NerdDinner.com 网站。 此时,NerdDinner.com 网站已经向我们进行身份验证,因此伪造的登录页可以直接重定向到该页面。 最终结果是攻击者有我们的用户名和密码,我们不知道我们向他们提供了它。

查看 AccountController LogOn 操作中的易受攻击代码

ASP.NET MVC 2 应用程序中 LogOn 操作的代码如下所示。 请注意,成功登录后,控制器将返回一个重定向到 returnUrl。 可以看到没有针对 returnUrl 参数执行验证。

代码清单 1 - ASP.NET MVC 2 "LogOn" 操作 AccountController.cs

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            FormsService.SignIn(model.UserName, model.RememberMe);
            if (!String.IsNullOrEmpty(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", "The user name or password provided is incorrect.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}

现在,让我们看看 ASP.NET MVC 3 LogOn 操作的更改。 此代码已更改,通过调用名为 IsLocalUrl()System.Web.Mvc.Url 帮助程序类的新方法来验证 returnUrl 参数。

代码清单 2 – ASP.NET MVC 3 LogOn 动作 AccountController.cs

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            FormsService.SignIn(model.UserName, model.RememberMe);
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", 
        "The user name or password provided is incorrect.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}

这已更改,通过调用 System.Web.Mvc.Url 帮助程序类 IsLocalUrl()中的新方法来验证返回 URL 参数。

保护 ASP.NET MVC 1.0 和 MVC 2 应用程序

可以在现有的 ASP.NET MVC 1.0 和 2 应用程序中,通过添加 IsLocalUrl() 辅助方法并更新 LogOn 操作以验证 returnUrl 参数,来利用 ASP.NET MVC 3 的更改。

UrlHelper IsLocalUrl() 方法实际上只调用 System.Web.WebPages 中的方法,因为 ASP.NET 网页应用程序也使用此验证。

列表 3 - ASP.NET MVC 3 的 UrlHelper 中的 IsLocalUrl() 方法 class

public bool IsLocalUrl(string url) {
    return System.Web.WebPages.RequestExtensions.IsUrlLocalToHost(
        RequestContext.HttpContext.Request, url);
}

IsUrlLocalToHost 方法包含实际的验证逻辑,如列表 4 所示。

清单 4:System.Web.WebPages RequestExtensions 类中的 IsUrlLocalToHost() 方法

public static bool IsUrlLocalToHost(this HttpRequestBase request, string url)
{
   return !url.IsEmpty() &&
          ((url[0] == '/' && (url.Length == 1 ||
           (url[1] != '/' && url[1] != '\\'))) ||   // "/" or "/foo" but not "//" or "/\"
           (url.Length > 1 &&
            url[0] == '~' && url[1] == '/'));   // "~/" or "~/foo"
}

在我们的 ASP.NET MVC 1.0 或 2 应用程序中,我们将向 AccountController 添加 IsLocalUrl() 方法,但建议尽可能将其添加到单独的帮助程序类。 我们将对 ASP.NET MVC 3 版本的 IsLocalUrl()进行两个小更改,以便它在 AccountController 中工作。 首先,我们将它从公共方法更改为私有方法,因为控制器中的公共方法可以作为控制器操作进行访问。 其次,我们将修改检查 URL 主机与应用程序主机的函数调用。 该调用使用 UrlHelper 类中的本地 RequestContext 字段。 我们将使用 this.Request.Url.Host,而不是使用 this.RequestContext.HttpContext.Request.Url.Host。 以下代码显示了修改后的 IsLocalUrl() 方法,用于 ASP.NET MVC 1.0 和 2 应用程序中的控制器类。

列出 5 - IsLocalUrl() 方法,该方法经过修改,可与 MVC 控制器类一起使用

private bool IsLocalUrl(string url)
{
   if (string.IsNullOrEmpty(url))
   {
      return false;
   }
   else
   {
      return ((url[0] == '/' && (url.Length == 1 ||
              (url[1] != '/' && url[1] != '\\'))) ||   // "/" or "/foo" but not "//" or "/\"
              (url.Length > 1 &&
               url[0] == '~' && url[1] == '/'));   // "~/" or "~/foo"
   }
}

现在 IsLocalUrl() 方法已到位,我们可以从 LogOn 操作调用该方法来验证 returnUrl 参数,如以下代码所示。

示例 6 – 更新后的 LogOn 方法以验证 returnUrl 参数

[HttpPost] 
public ActionResult LogOn(LogOnModel model, string returnUrl) 
{ 
    if (ModelState.IsValid) 
    { 
        if (MembershipService.ValidateUser(model.UserName, model.Password)) 
        { 
            FormsService.SignIn(model.UserName, model.RememberMe); 
            if (IsLocalUrl(returnUrl)) 
            { 
                return Redirect(returnUrl); 
            } 
            else 
            { 
                return RedirectToAction("Index", "Home"); 
            } 
        } 
        else 
        { 
            ModelState.AddModelError("", 
            "The user name or password provided is incorrect."); 
        } 
    }
}

现在,我们可以尝试使用外部返回 URL 登录,以测试开放重定向攻击。 用 /Account/LogOn?ReturnUrl=https://www.bing.com/ 再试一次。

显示“我的 M V C 应用程序登录”页的屏幕截图。标题栏突出显示并填充了外部返回 U R L。

图 04:测试更新的 LogOn 操作

成功登录后,我们将重定向到主页/索引控制器操作,而不是外部 URL。

显示“我的 M V C 应用程序索引”页的屏幕截图。

图 05:打开重定向攻击失败

总结

当重定向 URL 作为应用程序的 URL 中的参数传递时,可能会发生开放重定向攻击。 ASP.NET MVC 3 模板包括用于防范开放重定向攻击的代码。 可以通过 ASP.NET MVC 1.0 和 2 应用程序进行一些修改来添加此代码。 若要防止登录到 ASP.NET 1.0 和 2 应用程序时打开重定向攻击,请在 LogOn 操作中添加 IsLocalUrl() 方法并验证 returnUrl 参数。