6 марта 2013 г.

Защита от XSS в ASP.NET MVC, Часть 1

Если вы еще не страдали от XSS-атак на свое творение, то это только дело времени, или усилий, потраченных на защиту от них. О том, что же такое XSS, можно почитать в википедии.
Межсайтовый скриптинг - одна из самых распространенных уязвимостей в интернете. Даю гарантию, что как только вы представите свое творение-сайт широкой публике, то на него сразу же обрушится лавина попыток внедрить какую-либо пакость средствами XSS. Если в нескольких словах, то XSS (Сross Site Sсriрting) - вид уязвимости, которая позволяет внедрить в сайт вредоносный контент. Это может быть скрипт для кражи cookies, разметка для изменения контента, да и любой сторонний контент, который может как-то повлиять на нормальную работу сайта, или сломать всю вашу безопасность.

Сегодня мы начнем знакомиться с механизмами защиты от XSS-атак, а также посмотрим, как эту самую защиту можно обойти в случае необходимости.
Для демонстрации, я создал простое MVC-приложение, весь функционал которого будет заключаться в добавлении через сайт некого пользователя, и просмотр его информации после добавления.

Начать стоит с модели данных. Это будет класс Person, включающий всего 2 поля:

   1:  public class Person
   2:  {
   3:      [Required]
   4:      public string Name { get; set; }
   5:   
   6:      [DataType(DataType.MultilineText)]
   7:      public string Notes { get; set; }
   8:  }

На стартовой странице мы поместим форму добавления новой персоны, с заполнением полей модели Person. После добавления, мы будем перенаправлены на страницу Details, где сможем посмотреть данные только-что добавленного человека. За все это отвечает следующий код контроллера:

   1:  public ActionResult Index()
   2:  {
   3:      return View();
   4:  }
   5:   
   6:  [HttpPost]
   7:  public ActionResult Index(Person inputModel)
   8:  {
   9:      if (ModelState.IsValid)
  10:      {
  11:          TempData.Add("AddedPerson", inputModel);
  12:      }
  13:   
  14:      return RedirectToAction("Details");
  15:  }
  16:   
  17:  public ActionResult Details()
  18:  {
  19:      Person p = TempData["AddedPerson"] as Person;
  20:   
  21:      return View(p);
  22:  }

В обработчике POST-а, мы проверяем валидность принятой модели, добавляем данные в коллекцию TempData (для имитации записи в БД), и вызываем экшн Details, в котором достаем из TempData добавленную персону, и передаем представлению в качестве модели.

Для начала все готово.
Можно переходить к XSS.

Итак, давайте попробуем добавить нового пользователя, при этом введя в поле Notes не обычный текст:

После отправки формы, ASP.NET MVC сгенерирует ошибку:

ASP.NET понимает, что в посте были отправлены потенциально-опасные данные. И не важно, что в нашем случае, - это безобидный тэг strong. Для инфраструктуры это html-код, который может быть результатом межсайтового скриптинга. Это и есть первый, стандартный механизм защиты, присутствующий в MVC.

Но что же делать, если нам все-же крайне необходимо передать такой текст? Упустим причину, по которой нужен такой небезопасный функционал... Выход есть. И даже несколько.
Самым простым вариантом будет добавление к нужному методу контроллера экшн-фильтра ValidationInput:

   1:  [HttpPost]
   2:  [ValidateInput(false)]
   3:  public ActionResult Index(Person inputModel)
   4:  {
   5:      if (ModelState.IsValid)
   6:      {
   7:          TempData.Add("AddedPerson", inputModel);
   8:      }
   9:   
  10:      return RedirectToAction("Details");
  11:  }

Фильтр ValidationInput отключает проверку содержимого параметров запроса.
Теперь, при отправке формы с тэгами в текстовых полях, мы получим результат:

Как видите, данные добавились, но появилась новая проблема - наш <strong> отображается текстом, а не как html-тег. И это - второй механизм защиты MVC от XSS. По-умолчанию, при выводе каких-либо данных в представление, движок Razor кодирует весь html в текст. Стоит заметить, что такая прекрасная штука появилась лишь с версии MVC3. До этого приходилось каждый раз делать <%= HTML.Encode(Model.Val)>, как-то так, уже и не помню точно.

Но и эту стандартную защиту можно отключать при необходимости.
В представлении Details, есть строка, которая отображает поле Notes:

<div class="display-label">Notes: @Model.Notes</div>

Для того, чтобы предотвратить кодирование, необходимо воспользоваться специальным хэлпером Raw:

<div class="display-label">Notes: @Html.Raw(Model.Notes)</div>

Raw - не обращает внимание на формат содержимого, и вставляет его на страницу "как есть". В этом можно убедиться по итогу, где наш тэг успешно отработал:

На этом практически все. Осталась лишь одна мелочь.
Когда мы, в контроллере, воспользовались фильтром ValidateInput, то позволили всему экшн-у принимать потенциально опасные данные (включая поле Name, и все остальные поля, если бы они были). Таким образом, мы подвергаем опасности все свойства принимаемой модели, хотя на нужно лишь позволить html в одном поле Notes.
Решить эту проблему можно с помощью аннотации модели Person:

   1:  public class Person
   2:  {
   3:      [Required]
   4:      public string Name { get; set; }
   5:   
   6:      [DataType(DataType.MultilineText)]
   7:      [AllowHtml]
   8:      public string Notes { get; set; }
   9:  }

Здесь мы добавили к полю Notes правило, что это поле может содержать html. Теперь нам больше не нужен фильтр ValidateInput в контроллере, и его можно убрать:

   1:  [HttpPost]
   2:  public ActionResult Index(Person inputModel)
   3:  {
   4:      if (ModelState.IsValid)
   5:      {
   6:          TempData.Add("AddedPerson", inputModel);
   7:      }
   8:   
   9:      return RedirectToAction("Details");
  10:  }

Результат останется ожидаемым, в виде жирного слова на странице Details:

На этом все. Мы познакомились со стандартной защитой от межсайтового скриптинга в asp.net mvc, и намеренно обошли эту защиту.

В следующей части статьи, мы познакомимся с дополнительными, сторонними механизмами защиты от xss-атак. Это полезно для случаев, когда вам крайне-необходима возможность добавления пользователем потенциально-опасного контента.

_____
Исходники