Предотвращение атак с открытой переадресацией (C#)

Джон Галлоуэй

В этом руководстве объясняется, как предотвратить атаки с открытым перенаправлением в приложениях MVC ASP.NET. В этом руководстве рассматриваются изменения, внесенные в AccountController в ASP.NET MVC 3, и демонстрируется применение этих изменений в существующих приложениях ASP.NET MVC 1.0 и 2.

Что такое атака открытого перенаправления?

Любое веб-приложение, которое перенаправляет на URL-адрес, указанный с помощью запроса, например строку запроса или данные формы, может быть изменено для перенаправления пользователей на внешний вредоносный URL-адрес. Это незаконное изменение называется открытой атакой перенаправления.

Всякий раз, когда логика приложения перенаправляется на указанный URL-адрес, необходимо убедиться, что URL-адрес перенаправления не был изменен. Имя входа, используемое в AccountController по умолчанию для ASP.NET MVC 1.0 и ASP.NET MVC 2, уязвимо для открытых атак перенаправления. К счастью, можно легко обновить существующие приложения для использования исправлений из предварительной версии ASP.NET MVC 3.

Чтобы понять уязвимость, давайте рассмотрим, как работает перенаправление входа в проекте веб-приложения MVC 2 по умолчанию ASP.NET. В этом приложении попытка посетить действие контроллера с атрибутом [Авторизовать] перенаправляет неавторизованных пользователей в представление /Account/LogOn. Это перенаправление на /Account/LogOn будет включать параметр returnUrl querystring, чтобы пользователь смог вернуться к первоначально запрошенному URL-адресу после успешного входа в систему.

На снимке экрана ниже видно, что попытка получить доступ к представлению /Account/ChangePassword, если не вошли в систему, приводит к перенаправлению в /Account/LogOn? ReturnUrl=%2fAccount%2fChangePassword%2f.

Снимок экрана: страница входа в приложение My M V C. Выделена строка заголовка.

Рис. 01. Страница входа с открытым перенаправлением

Так как параметр returnUrl querystring не проверяется, злоумышленник может изменить его, чтобы внедрить в параметр любой 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.

Снимок экрана: домашняя страница Nerd Dinner dot com. Строка заголовка выделена и заполнена U R L, который указывает на Nerd Diner dot com.

Рис. 02. Страница входа NerdDinner с открытым перенаправлением

При правильном входе действие LogOn ASP.NET учетной записи MVCController перенаправляет нас по URL-адресу, указанному в параметре returnUrl querystring. В этом случае это URL-адрес, введенный злоумышленником, то есть http://nerddiner.com/Account/LogOn. Если мы не будем очень бдительными, скорее всего, мы не замечаем этого, особенно потому, что злоумышленник был осторожен, чтобы убедиться, что его подделаная страница выглядит точно так же, как и законная страница входа. Эта страница входа содержит сообщение об ошибке с запросом на повторный вход. Неуклюжий нас, мы, должно быть, неправильно введите пароль.

Снимок экрана: страница входа в кованый Nerd Dinner, предлагающая пользователю повторно ввести свои учетные данные. Выделен кованый URL-адрес в строке заголовка.

Рис. 03. Подделанный экран входа в NerdDinner

При повторном вводе имени пользователя и пароля подделаная страница входа сохраняет информацию и отправляет нас обратно на законный NerdDinner.com сайт. На этом этапе сайт NerdDinner.com уже прошел проверку подлинности, поэтому подделаемая страница входа может перенаправляться непосредственно на эту страницу. В результате у злоумышленника есть имя пользователя и пароль, и мы не знаем, что предоставили его.

Просмотр уязвимого кода в действии LogOn AccountController

Ниже показан код действия LogOn в приложении ASP.NET MVC 2. Обратите внимание, что при успешном входе контроллер возвращает перенаправление на returnUrl. Вы видите, что проверка параметра returnUrl не выполняется.

Листинг 1. ASP.NET действие LogOn 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);
}

Теперь давайте рассмотрим изменения в действии logOn ASP.NET MVC 3. Этот код был изменен для проверки параметра returnUrl путем вызова нового метода во вспомогательном классе System.Web.Mvc.Url с именем IsLocalUrl().

Листинг 2. ASP.NET действие LogOn 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);
}

Он был изменен для проверки возвращаемого параметра URL-адреса путем вызова нового метода во вспомогательном классе System.Web.Mvc.Url, IsLocalUrl().

Защита приложений ASP.NET MVC 1.0 и MVC 2

Мы можем воспользоваться преимуществами изменений ASP.NET MVC 3 в существующих приложениях ASP.NET MVC 1.0 и 2, добавив вспомогательный метод IsLocalUrl() и обновив действие LogOn для проверки параметра returnUrl.

Метод UrlHelper IsLocalUrl() фактически просто вызывает метод в System.Web.WebPages, так как эта проверка также используется веб-страницы ASP.NET приложениями.

Листинг 3. Метод IsLocalUrl() из ASP.NET MVC 3 UrlHelper class

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

Метод IsUrlLocalToHost содержит фактическую логику проверки, как показано в листинге 4.

Листинг 4 . Метод IsUrlLocalToHost() из класса System.Web.WebPages RequestExtensions

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, но по возможности рекомендуется добавить его в отдельный вспомогательный класс. Мы вносим два небольших изменения в ASP.NET версии MVC 3 IsLocalUrl(), чтобы она работала внутри AccountController. Во-первых, мы изменим его с открытого метода на частный, так как открытые методы в контроллерах могут быть доступны как действия контроллера. Во-вторых, мы изменим вызов, который проверяет узел URL-адресов по отношению к узлу приложения. Этот вызов использует локальное поле RequestContext в классе UrlHelper. Вместо этого. 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. Обновлен метод 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/ снова.

Снимок экрана: страница входа в приложение My M V C. Строка заголовка выделена и заполнена внешним возвращаемым url-адресом.

Рис. 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() и проверьте параметр returnUrl в действии LogOn.