預防開啟重新導向攻擊 (C#)
作者 :JonGaloway
本教學課程說明如何在 ASP.NET MVC 應用程式中防止開啟重新導向攻擊。 本教學課程討論在 ASP.NET MVC 3 中 AccountController 中所做的變更,並示範如何在現有的 ASP.NET MVC 1.0 和 2 應用程式中套用這些變更。
什麼是開放式重新導向攻擊?
任何重新導向至透過要求指定的 URL 的 Web 應用程式,例如 querystring 或表單資料,都可能會遭到竄改,以將使用者重新導向至外部惡意 URL。 這項竄改稱為開放式重新導向攻擊。
每當應用程式邏輯重新導向至指定的 URL 時,您必須確認重新導向 URL 尚未遭到竄改。 ASP.NET MVC 1.0 和 ASP.NET MVC 2 的預設 AccountController 中使用的登入容易受到開啟的重新導向攻擊。 幸運的是,您可以輕鬆地更新現有的應用程式,以使用來自 ASP.NET MVC 3 Preview 的更正。
若要瞭解弱點,讓我們看看登入重新導向在預設 ASP.NET MVC 2 Web 應用程式專案中的運作方式。 在此應用程式中,嘗試流覽具有 [Authorize] 屬性的控制器動作,會將未經授權的使用者重新導向至 /Account/LogOn 檢視。 此重新導向至 /Account/LogOn 將包含 returnUrl 查詢字串參數,讓使用者可以在成功登入之後傳回原始要求的 URL。
在下列螢幕擷取畫面中,我們可以看到未登入時嘗試存取 /Account/ChangePassword 檢視會導致重新導向至 /Account/LogOn?ReturnUrl=%2fAccount%2fChangePassword%2f。
圖 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,這在晚餐字組中遺漏了 「n」。 在此範例中,這是攻擊者所控制的網域。 當我們存取上述連結時,我們會前往合法的 NerdDinner.com 登入頁面。
圖 02:具有開啟重新導向的 NerdDinner 登入頁面
當我們正確登入時,ASP.NET MVC AccountController 的 LogOn 巨集指令會將我們重新導向至 returnUrl 查詢字串參數中指定的 URL。 在此情況下,這是攻擊者輸入的 URL,也就是 http://nerddiner.com/Account/LogOn
。 除非非常謹慎,否則不太可能注意到這點,特別是因為攻擊者已小心確保其偽造頁面看起來與合法登入頁面完全相同。 此登入頁面包含錯誤訊息,要求我們再次登入。 Clumsy us,我們必須輸入錯誤的密碼。
圖 03:偽造的 NerdDinner 登入畫面
當我們重新輸入使用者名稱和密碼時,偽造的登入頁面會儲存資訊,並將我們傳回合法 NerdDinner.com 網站。 此時,NerdDinner.com 網站已驗證過我們,因此偽造的登入頁面可以直接重新導向至該頁面。 最終結果是攻擊者具有我們的使用者名稱和密碼,而且我們不知道已提供給他們。
查看 AccountController LogOn Action 中的易受攻擊程式碼
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 應用程式
我們可以藉由新增 IsLocalUrl () 協助程式方法和更新 LogOn 巨集指令來驗證 returnUrl 參數,利用現有 ASP.NET MVC 1.0 和 2 應用程式的 ASP.NET MVC 3 變更。
UrlHelper IsLocalUrl () 方法實際上只是在 System.Web.WebPages 中呼叫方法,因為此驗證也會由 ASP.NET Web Pages應用程式使用。
清單 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 應用程式中,我們會將 IsLocalUrl () 方法新增至 AccountController,但建議您盡可能將其新增至個別的協助程式類別。 我們會對 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/ 再次。
圖 04:測試更新的 LogOn 動作
成功登入之後,我們會重新導向至 Home/Index Controller 動作,而不是外部 URL。
圖 05:開放式重新導向攻擊失敗
摘要
當重新導向 URL 以應用程式 URL 中的參數傳遞時,可能會發生開啟重新導向攻擊。 ASP.NET MVC 3 範本包含程式碼,可防範開放式重新導向攻擊。 您可以對 ASP.NET MVC 1.0 和 2 應用程式進行一些修改來新增此程式碼。 若要防止登入 ASP.NET 1.0 和 2 應用程式時開啟重新導向攻擊,請新增 IsLocalUrl () 方法,並在 LogOn 巨集指令中驗證 returnUrl 參數。