Andrey on .NET | Часть 15 – Проверка запроса

Часть 15 – Проверка запроса

Никогда не доверяйте значениям, которые ввели пользователи. Сколько раз вы слышали эту фразу? А сколько раз следовали ей? В процессе работы веб-приложения вполне может встретиться ситуация, когда передаваемые на сервер данные могут содержать XSS, XSRF, инъекции SQL кода и т.д. Рассмотрим что произойдет с создаваемым веб-приложением в этом случае.

Перед началом необходимо отметить, что речь пойдет не о проверке данных введенных пользователем как таковых. Разберем вопрос проверки самого запроса, сделанного на сервер.

Создаем площадку для тестов

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

Для достижения указанной цели добавим в класс UserProfileModel новое свойство Description:

namespace MVCDemo.Models
{
    using System.ComponentModel.DataAnnotations;
    using MVCDemo.Attributes.Validation;
    using MVCDemo.Resources.Models;
    using MVCDemo.Resources.Shared;

    public class UserProfileModel
    {
        .........

        [Display(Name = "Homepage", ResourceType = typeof(UserProfileRes))]
        [DataType(DataType.Url)]
        [RegularExpression(@"(http(s)?://)?([\w-]+\.)+[\w-]+(/[\w- ;,./?%&=]*)?",
            ErrorMessageResourceName = "InvalidUrl",
            ErrorMessageResourceType = typeof(ErrorsRes))]
        [StringLength(96,
            ErrorMessageResourceName = "StringTooLong",
            ErrorMessageResourceType = typeof(ErrorsRes))]
        public string Homepage { get; set; }

        [Display(Name = "Description", ResourceType = typeof(UserProfileRes))]
        [DataType(DataType.MultilineText)]
        [StringLength(255,
            ErrorMessageResourceName = "StringTooLong",
            ErrorMessageResourceType = typeof(ErrorsRes))]
        public string Description { get; set; }
    }
}

Имя этого свойства внесем в файл с ресурсами для данного класса:

Файл Resources\Controllers\UserProfilesRes.resx
Description Description Дополнительная информация о пользователе

Кроме того, необходимо внести изменение в Представление UserProfiles\Index. Для отображения значения, содержащегося в Description, воспользуемся методом Html.Raw(). Это гарантирует, что при его выводе теги попадут в HTML код страницы, а не будут перекодированы в текст.

@model IEnumerable<MVCDemo.Models.UserProfileModel>
@using MVCDemo.Resources.Views.UserProfiles;
@using MVCDemo.Resources.Models;
@{
    ViewBag.Title =  IndexRes.PageTitle;
}
<h2>@ViewBag.Title</h2>
<p>@ViewBag.Message</p>
<p>@Html.ActionLink(IndexRes.CreateNewLink, "Create")</p>
<table>
    <tr>
        <th>@UserProfileRes.Login</th>
        <th>@UserProfileRes.DisplayName</th>
        <th>@UserProfileRes.Email</th>
        <th>@UserProfileRes.Homepage</th>
        <th>@UserProfileRes.Description</th>
    </tr>
    @foreach (var item in Model) {
        <tr>
            <td>@item.Login</td>
            <td>@item.DisplayName</td>
            <td>@item.Email</td>
            <td>@item.Homepage</td>
            <td>@Html.Raw(item.Description)</td>
        </tr>
    }
</table>

И, разумеется, изменим UserProfiles\Create, добавив в него соответствующее поле для ввода:

@model MVCDemo.Models.NewUserProfileModel
@using MVCDemo.Resources.Views.UserProfiles;
@{
    ViewBag.Title = CreateRes.PageTitle;
}

<h2>@ViewBag.Title</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/Validation/EqualAttribute.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>

        .........

        <div class="editor-label">
            @Html.LabelFor(model => model.Homepage)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Homepage)
            @Html.ValidationMessageFor(model => model.Homepage)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Description)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Description)
            @Html.ValidationMessageFor(model => model.Description)
        </div>

        <div class="editor-label">
            @Html.EditorFor(model => model.AgreementAccepted)
            @Html.LabelFor(model => model.AgreementAccepted)
        </div>
        <div class="editor-field">
            @Html.ValidationMessageFor(model => model.AgreementAccepted)
        </div>

        <p><input type="submit" value="@CreateRes.CreateProfileButton" /></p>
    </fieldset>
}

<div>@Html.ActionLink(CreateRes.BackToProfilesList, "Index")</div>

Кроме того, временно отключим проверку данных на стороне клиента:

<appSettings>
  <add key="ClientValidationEnabled" value="false"/> 
  <add key="UnobtrusiveJavaScriptEnabled" value="false"/> 
</appSettings>

Поставим точку останова в самом начале метода Create(NewUserProfileModel newUserProfile) и запустим веб-приложение. Теперь заполним все поля корректными значениями, при этом обязательно заполнив поле Description текстом с HTML тегами. Нажмем кнопку "Create" и …

Можно сразу отметить, что выполнение веб-приложения не дошло до отмеченной в Контроллере точки. Вместо этого было выброшено исключение с текстом "A potentially dangerous Request.Form value was detected from the client (Description=...". Откуда оно взялось?

Проверка запроса в ASP.NET

Ядро ASP.NET, еще начиная с версии 1.1, для защиты от XSS, XSRF и прочих атак осуществляет проверку получаемых запросов. В них, среди полученных значений, производится поиск следующих последовательностей: "<!", "</", "<[символ латинского алфавита]", "&#". В случае их успешного обнаружения, происходит выброс исключения HttpRequestValidationException.

В ASP.NET 4.0 появилась возможность расширять правила проверки. Благодаря этому ASP.NET MVC 3 позволяет осуществлять более гибкое управление проверкой запроса. Рассмотрим как это работает.

Но прежде необходимо отметить два следующих факта:

  1. Вмешиваясь в механизм защиты от атак, разработчик берет на себя еще больше ответственности за контроль получаемых данных. Это делает еще более актуальным правило: "никогда не доверяйте полученным от пользователя значениям".
  2. Процедура проверки данных и самого запроса не одно и тоже. Даже если отключить проверку последнего, то это не отменит проверку самих данных согласно установленным правилам.

Так же стоит отметить, что при разрешенном вводе HTML тегов необходимо отслеживать не только возможные атаки, но и корректность самого HTML кода. Например, вывод пользовательского текста с единственным не закрытым тегом <div> может легко нарушить всю структуру сайта. Поэтому часто используют собственные ограниченные наборы тегов, приводя их HTML аналогам уже при выводе. Яркий пример – теги на форумах: [b], [i], [code] и т.д. Непосредственный ввод HTML тегов используется только при его крайней необходимости (например, в панели управления сайтом и т.д).

Разрешаем ввод HTML тегов

Отключаем проверку запроса

ASP.NET MVC позволяет отключить проверку запроса для выбранного действия. Для этого его необходимо отметить атрибутом [ValidateInput] c значением false.

[ValidateInput(false)]
public ActionResult Create(NewUserProfileModel newUserProfile)

Теперь попробуем отправить запрос повторно. Выброс исключения не произойдет и данные будут обработаны Действием Create(). Кстати, можно ввести некорректные значения и убедиться что проверка данных по прежнему осуществляется.

У такого решения есть явный минус: теперь в любое из полей можно ввести HTML теги. Ничего не мешает создать имя входа вроде "<b>SuperUser</b>". Единственные свойства, которые не пропустят символы "<" в своих значениях, это Email и Homepage. У них сработают правила, заданные регулярными выражениями. Для остальных свойств необходимо реализовать проверку.

Получение параметров запроса

В некоторых Действиях может использоваться непосредственное обращение к параметрам запроса. Рассмотрим вот такой пример:

[ValidateInput(false)]
public ActionResult Index()
{
    string name = Request.QueryString["name"];

    .........
}

Приведенный код приведет к исключению HttpRequestValidationException, если в запросе параметр name будет содержать небезопасные последовательности. Это произойдет не смотря даже на то, что у Действия Index указан атрибут [ValidateInput(false)].

Чтобы в методе Действия получить исходные значения запроса, необходимо воспользоваться вспомогательным методом Unvalidated(), который объявлен в пространстве имен System.Web.Helpers:

using System.Web.Helpers;

.........

[ValidateInput(false)]
public ActionResult Index()
{
    var queryString = Request.Unvalidated().QueryString;
    string name = queryString["name"];

    .........

}

Выборочное исключение полей из проверки запросов в ASP.NET MVC 3

Согласитесь, что не очень логично отключать проверку всего запроса из-за одного поля, а затем реализовывать её самостоятельно. Поэтому в ASP.NET MVC 3 появилась возможность выборочно исключать поля из проверки запроса. Для этого выбранное свойство необходимо отметить атрибутом [AllowHtml], расположенным в пространстве имен System.Web.Mvc.

Давайте уберем атрибут [ValidateInput(false)] у Действий Контроллера. Затем откроем исходный код класса UserProfileModel. И присвоим свойству Description атрибут [AllowHtml], т.к. только для него необходимо разрешить ввод HTML тегов.

namespace MVCDemo.Models
{
    .........

    using System.Web.Mvc;

    .........

    public class UserProfileModel
    {
        .........

        [Display(Name = "Description", ResourceType = typeof(UserProfileRes))]
        [DataType(DataType.MultilineText)]
        [StringLength(255,
            ErrorMessageResourceName = "StringTooLong",
            ErrorMessageResourceType = typeof(ErrorsRes))]
        [AllowHtml]
        public string Description { get; set; }
    }
}

Запустим веб-приложение. Заполним форму и убедимся что все поля, за исключением Description, не дают возможности ввести запрещённые последовательности символов.

При использовании [AllowHtml], для обращения в Контроллере к данным запроса также используется метод Unvalidated(). Но при этом нет необходимости отмечать само Действие атрибутом [ValidateInput].

Включим проверку на стороне клиента, установив в web.config параметрам ClientValidationEnabled и UnobtrusiveJavaScriptEnabled значение true.

Сообщение о ошибке, выводимое при проверки запроса, сложно назвать дружественным к пользователю. Постараемся в дальнейшем это исправить.


Исходный код проекта (C#, Visual Studio 2010): MVCDemo-Part15.zip (478 Kb)

Pingbacks and trackbacks (3)+

Добавить комментарий