作者 :喬恩·蓋洛韋
本教學說明如何在 ASP.NET MVC 應用程式中防止開放式重定向攻擊。 本教學將討論在 ASP.NET MVC 3 中對 AccountController 所做的變更,並示範如何在您現有的 ASP.NET MVC 1.0 和 MVC 2 應用程式中應用這些變更。
什麼是開放式轉向攻擊?
任何將 URL 重定向到請求指定的網頁應用程式,例如查詢字串或表單資料,都可能被竄改,將使用者導向外部惡意 URL。 這種竄改稱為開放式重新導向攻擊。
每當應用程式邏輯重新導向至指定的 URL 時,您都必須確認重新導向 URL 並未遭到竄改。 ASP.NET MVC 1.0 和 ASP.NET MVC 2 的預設 AccountController 所使用的登入方式容易受到開放重定向攻擊。 幸運的是,更新現有應用程式以使用 ASP.NET MVC 3 預覽版的修正非常簡單。
為了了解這個漏洞,讓我們看看 MVC 2 網頁應用程式專案預設 ASP.NET 登入重定向是如何運作的。 在此應用程式中,嘗試造訪帶有 [Authorize] 屬性的控制器動作時,未經授權的使用者會被重新導向至 /Account/LogOn 視圖。 此重定向至 /Account/LogOn 會包含 returnUrl 查詢字串參數,讓使用者在成功登入後能返回原本請求的網址。
在下面的截圖中,我們可以看到嘗試在未登入時存取 /Account/ChangePassword 檢視時,會被重定向到 /Account/LogOn?ReturnUrl=%2fAccount%2fChangePassword%2f。
圖 01:具有開放式重定向的登入頁面
由於 ReturnUrl 查詢字串參數未被驗證,攻擊者可修改參數,將任意 URL 位址注入參數中,進行開放式重定向攻擊。 為了示範,我們可以將 ReturnUrl 參數改為 https://bing.com,因此最終的登入網址會是 /Account/LogOn?ReturnUrl=https://www.bing.com/. 成功登入網站後,我們將被導向 https://bing.com。 由於這個重定向未經驗證,可能會指向試圖欺騙使用者的惡意網站。
更複雜的開放式重定向攻擊
開放式重定向攻擊尤其危險,因為攻擊者知道我們正在嘗試登入特定網站,這使我們容易成為 網路釣魚攻擊的受害者。 例如,攻擊者可能會向網站使用者發送惡意電子郵件,試圖竊取他們的密碼。 讓我們來看看這在 NerdDinner 網站上會如何運作。 (請注意,NerdDinner 現場網站已更新以防範公開重定向攻擊。)
首先,攻擊者會寄給我們一個連結,指向 NerdDinner 的登入頁面,並附帶一個指向他們偽造頁面的重定向:
http://nerddinner.com/Account/LogOn?returnUrl=http://nerddiner.com/Account/LogOn
請注意,回傳網址指向 nerddiner.com,但 dinner 這個詞缺少一個「n」。 在這個例子中,這是攻擊者所控制的領域。 當我們點擊上述連結時,會被帶到合法的 NerdDinner.com 登入頁面。
圖 02:NerdDinner 登入頁面,開放重定向
當我們正確登入時,ASP.NET MVC AccountController 的 LogOn 動作會將我們導向到 returnUrl 查詢字串參數中指定的網址。 在此情況下,這個網址就是攻擊者所輸入的 http://nerddiner.com/Account/LogOn。 除非我們非常小心,否則很可能不會注意到這點,尤其是因為攻擊者一直小心確保他們偽造的頁面看起來和合法的登入頁面一模一樣。 此登入頁面包含錯誤訊息,要求我們重新登入。 我們真笨,一定是密碼打錯了。
圖03:Forged NerdDinner登入畫面
當我們重新輸入使用者名稱和密碼時,偽造的登入頁面會儲存資訊,並送回合法的 NerdDinner.com 網站。 此時,NerdDinner.com 網站已經驗證了我們的身份,因此偽造的登入頁面可以直接導向該頁面。 結果是攻擊者擁有我們的使用者名稱和密碼,而我們卻不知道我們已經提供給他們。
查看 AccountController 登入動作中的漏洞程式碼
ASP.NET MVC 2 應用程式中 LogOn 動作的程式碼如下所示。 請注意,成功登入後,控制器會回傳一個 returnURL 的重定向。 你可以看到沒有針對 returnUrl 參數進行驗證。
列表 1 – ASP.NET MVC 2 登入操作 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 登入動作的變更。 此程式碼已修改,透過呼叫 System.Web.Mvc.Url 輔助類別中一個名為 IsLocalUrl()的新方法來驗證 returnUrl 參數。
列表 2 – ASP.NET MVC 3 登入操作 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應用程式
我們可以透過在現有的 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.RequestContext.HttpContext.Request.Url.Host,我們將使用 this.Request.Url.Host。 以下程式碼展示了修改後的 IsLocalUrl() 方法,適用於 ASP.NET MVC 1.0 和 MVC 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.");
}
}
}
現在我們可以嘗試使用外部回傳網址登入,來測試開放式重定向攻擊。 讓我們再使用 /Account/LogOn?ReturnUrl=https://www.bing.com/。
圖 04:測試更新後的 LogOn 動作
成功登入後,我們會被導向到「主頁/索引控制器」操作,而不是外部網址。
圖 05:被擊退的開放式重定向攻擊
總結
當 URL 參數中包含重定向的 URL 時,可能會發生開放式重定向攻擊。 ASP.NET MVC 3 範本包含防範開放式重定向攻擊的程式碼。 你可以經過修改,將這些程式碼加入 ASP.NET MVC 1.0和MVC 2應用程式。 為了防止在 ASP.NET 1.0 和 2.0 應用程式中登入時遭受開放式重定向攻擊,請新增一個 IsLocalUrl() 方法,並在 LogOn 動作中驗證 returnUrl 參數。