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

