Простой способ обезопаситься от CSRF в ASP.Net MVC

Наверное, все, кто занимается веб-разработкой слышали о CSRF-уязвимостях (читается sea-surf). Если быть кратким, то если у вас на сайте есть ссылки вроде http://mysite.ru/Articles/Delete/24, по которой удаляется статья с 24-ым айдишником (естественно, с проверкой прав доступа), то если авторизовавшийся на вашем сайте админ зайдет на какой-нибудь зловредный сайт, в котором подсонут яваскрипт вроде

window.location.href = "http://mysite.ru/Articles/Delete/24"

то статья с вашего сайта успешно удалится. Конечно, использовать такие уязвимости достаточно сложно, и наврядли атаку подобного типа проведут на сайт с посещаемостью в 1-2 тысячи человек. Но если вы планируете раскрутиться до уровня твиттера… :)
Впрочем, оставлять лазейки в безопасности не хочется, даже если сайт пишется для «внутренних целей», что уж говорить про продакшен.

Тем более, что MVC3 позволяет с лёгкостью таких атак избежать.

Много раз писалось, что изменение данных должно происходить только в результате POST-запросов, а по GET-у данные модифицироваться не должны. Однако для действий вроде удаления, когда никаких дополнительных данных от пользователя не требуется, так и тянет использовать обычные ссылки — их очень просто вставить (@Html.ActionLink), это работает даже с IE 1.0, это генерит минимум хтмл-кода. В общем, отговорок можно найти много :)
Но если сделать добавление «POST-ссылок» таким же удобным и обратно-совместимым, то никаких проблем в использовании не возникнет. Поэтому я и решил написать небольшой хелпер, который по простоте использования не уступал бы Html.ActionLink, но создавал бы простую форму и при клике по ссылке сабмитил бы её. Естественно, пользователям с современными браузерами видеть эту форму необязательно, а на браузерах, не поддерживающих яваскрипт (или с отключенными скриптами), наоборот, должна вместо ссылки показываться кнопка сабмита формы. Выглядеть это должно как-то так:

Для людей с яваскриптом: И для людей без него:

А использоваться — так:

@Helper.PostLink("del", "Delete", new { title = title }) //del - отображаемый текст, Delete - имя экшена в контроллере. параметры абсолютно эквивалентны параметрам @Html.ActionLink()

Если стало любопытно и хочется использовать это у себя, то вот как выглядит собственно хелпер:

@helper PostLink(string link, string action, object routeValues, object htmlAttributes = null)
    {
        var id = Html.Guid();
        RouteValueDictionary attributes;
        if (htmlAttributes == null)
        {
            attributes = new RouteValueDictionary();
        }
        else
        {
            attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
        }
    <text>
    <form method="post" action="@Url.Action(action, routeValues)" id="form-@id">
    @Html.AntiForgeryToken()
    <input type="submit" value="@link" />
    </form>
    @Html.ActionLink(link, action, new RouteValueDictionary(routeValues), attributes.MergeWith(new { id = "link-" + id, style = "display: none;" }))
    <script type="text/javascript">
        $("#form-@id").css("display", "none");
        $("#link-@id").show();
        $("#link-@id").live('click', function (ev) {
            ev.preventDefault();
            $("#form-@id").submit();
        });
    </script>
    </text>
}

Мы используем форму для отправления POST-запроса (именно она будет показана в случае выключенного яваскрипта), @Html.AntiForgeryToken() для защиты формы от CSRF (не забудьте повесить на экшн в контроллере атрибуты [HttpPost] и [ValidateAntiForgeryToken]) и jQuery, чтобы спрятать форму и оставить только ссылку (если есть яваскрипт).

Если любопытно увидеть код целиком, то можно скачать тестовый проект (один контроллер-одна вьюшка) демонстрирующий функционал.

P.S. Пара экстеншен-методов используемых в хэлпере (если лень качать тестовый проект, а скомпилить код всё-таки хочется :)):


    public static class Extensions
    {
        private static Random _random = null;
        private static Random Random
        {
            get
            {
                return _random ?? (_random = new Random());
            }
        }
        public static string Guid(this HtmlHelper helper)
        {
            var bytes = new byte[16];
            Random.NextBytes(bytes);

            return new Guid(bytes).ToString();
        }

        public static IDictionary<string, object> MergeWith(this IDictionary<string, object> dict, object values)
        {
            var valuesDict = HtmlHelper.AnonymousObjectToHtmlAttributes(values);
            foreach (var keyValuePair in valuesDict)
            {
                dict[keyValuePair.Key] = keyValuePair.Value;
            }

            return dict;
        }
    }
Опубликовать в Facebook
Опубликовать в Google Plus

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *