阻止打开重定向攻击 (C#)

作者 :Jon Galloway

本教程介绍如何在 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 querystring 参数,以便用户可以在成功登录后返回到最初请求的 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 在单词 dinner 中缺少“n”。 在此示例中,这是攻击者控制的域。 访问上述链接时,会进入合法的 NerdDinner.com 登录页面。

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

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

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

显示伪造的 Nerd 晚餐登录页面的屏幕截图,提示用户重新输入其凭据。突出显示标题栏中的伪造 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 帮助程序类 中调用新方法来验证返回 URL 参数, IsLocalUrl()对此进行了更改。

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

通过添加 IsLocalUrl () 帮助程序方法并更新 LogOn 操作以验证 returnUrl 参数,可以利用现有 ASP.NET MVC 1.0 和 2 应用程序中 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 () 方法,但建议你将其添加到单独的帮助程序类(如果可能)。 我们将对 isLocalUrl () 的 ASP.NET MVC 3 版本进行两个小更改,使其在 AccountController 中正常工作。 首先,我们将它从公共方法更改为专用方法,因为控制器中的公共方法可以作为控制器操作进行访问。 其次,我们将修改针对应用程序主机检查 URL 主机的调用。 该调用使用 UrlHelper 类中的本地 RequestContext 字段。 而不是使用此。RequestContext.HttpContext.Request.Url.Host,我们将使用它。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 - 更新了用于验证 returnUrl 参数的 LogOn 方法

[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 应用程序时防范开放重定向攻击,请添加 IsLocalUrl () 方法并在 LogOn 操作中验证 returnUrl 参数。