Наверное, все, кто занимается веб-разработкой слышали о 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; } }