Безопасность веб-сайтов ASP.NET MVC
«Ломать – не строить», гласит народная мудрость. Чтобы создать полезное для пользователей веб-приложение, приходится прикладывать много усилий, придумывать нужную функциональность и реализовывать ее. Чтобы сломать веб-приложение, существует набор стандартных методик, рассчитанных на то, что разработчик веб-приложения не подумал или поленился защитить свое веб-приложение от определенного типа атак.
Сторонники «хакерских игр» могут возмутиться, что взлом веб-сайта тоже сложная и кропотливая работа. Так-то оно, может быть, и так, однако по своей сути взлом в подавляющем большинстве случаев основан на стандартных подходах и оставленных разработчиком лазейках. В конечном итоге изобрести колесо сложнее, чем придумать, как проколоть его гвоздем, хотя последнее тоже может быть увлекательной задачей.
Таким образом, у нападающего на сайт взломщика есть набор практик и инструментов, позволяющих пробовать стандартные подходы для взлома веб-сайта, поэтому веб-разработчик, создающий веб-приложение, должен также иметь собственный список задач по обеспечению безопасности веб-приложения. Основные советы я привожу в этой небольшой статье. Позднее я напишу более подробно о нескольких самых распространенных атаках.
Базовые советы по обеспечению безопасности веб-приложений
1. Валидация пользовательского ввода
При получении любых данных от пользователя, обязательно должна осуществляться проверка на размер и формат данных, применение ограничений по допустимым значениям, предусмотренных бизнес логикой и проверка данных на безопасность. В случае, если с данными что-то не так, то существует два стандартных подхода – либо вернуть их пользователю с ошибкой, либо попытаться их «очистить». Второй путь часто выбирают для облегчения жизни пользователя, например при вводе даты или времени в неверном формате, однако этот путь опасен не только с точки зрения возможных ошибок в коде, «очищающем» данные, так и с точки зрения нарушения логики приложения.
Основная рекомендация – отклонять некорректные данные и предлагать пользователю ввести данные в правильном формате.
Сложности возникают в классической задаче: ввод HTML тегов для оформления публикуемых на сайте текстов. Если пытаться вырезать недопустимые теги, существует большая вероятность, что злоумышленник придумает способ, как обойти «вырезалку» теги. Если же отклонять введенный текст, то пользователь может быть очень расстроен тем, что не понимает, из-за чего его текст был отклонен и где в нем недопустимый тег. Здесь на помощь приходит следующий совет.
Для часто используемых проверок имеет смысл создать вспомогательный класс, содержащий методы для проверки вводимых данных, тогда код может выглядеть как-то так:
public ActionResult ProcessData ( DataObject data )
{
// Валидация
if (! SomeValidationLogic.ValidateSomeData (data.DataField) )
ModelState.AddModelError ("DataField", "DataField is invalid");
// ... там дальше много кода
}
В свое время для MVC Framework 1.0 создавались различные библиотеки валидации вроде Validator Toolkit for ASP.NET MVC или xVal – можно посмотреть на их устройство для общего развития. Из неплохих современных фреймворков для валидации данных существует FluentValidation.
В случае если используются механизмы валидации данных на клиенте, они должны быть продублированы на сервере. Злоумышленнику ничего не стоит модифицировать клиентские страницы либо эмулировать передачу данных со страниц на сервер. Относитесь к коду клиентской валидации как к возможности сэкономить время пользователя при подготовке данных в нужном формате, а не как к дополнительной степени защиты прилжения.
2. Форматирование вывода пользовательских данных
При публикации данных, так или иначе пришедших из внешних источников, на страницах вашего веб-приложения, обязательно нужно исключать возможность атак, подразумевающих внедрение вредоносного кода на страницы. Да и просто «кривая» разметка может испортить внешний вид страниц и отпугнуть пользователей вашего сайта, поэтому корректное форматирование текстов всегда является хорошим выходом.
Для того чтобы избежать проблем с выводом разметки, там где вы ее не ожидаете, используйте метод Html.Encode(). Если же нужно допустить вывод некоторых тегов, то вы можете воспользоваться методами AntiXss.HtmlEncode() и AntiXss.HtmlAttributeEncode(), реализованном в библиотеке AniXSS, входящей в проект Microsoft Web Protection Library. В библиотеке AniXSS также доступен метод AntiXss.GetSafeHtmlFragment(), позволяющий выводить «безопасные» теги, вырезая «опасные» и корректно форматируя разметку.
Библиотеку AntiXss можно зарегистрировать в для использования по умолчанию для кодирования HTML разметки, об этом написано пошаговое руководство в блоге у Фила Хаака.
3. Корректное использование метода POST и защищенного протокола HTTPS
Данные, которые приложение ожидает получить методом POST, должны приходить методом POST, для этого достаточно использовать атрибут HttpPost на действиях контроллеров, ожидающих данные методом POST – таким образом можно защититься от атак, заставляющих добропорядочных пользователей переходить по ссылкам, передающим данные методом GET.
Хорошая практика использовать метод POST при выполнении важных действий, таких как удаление или изменение данных, поскольку злоумышленник легко может подставить пользователю ссылку вроде /somecontroller/delete/1 с другого сайта, заставив авторизованного ранее на вашем сайте пользователя, выполнить не всегда полезное действие.
Для данных, имеющих большую ценность, например, таких как персональные данные и данные о кредитных картах, стоит использовать безопасный протокол передачи данных HTTPS. При этом, сами формы, запрашивающие данные, должны быть отправлены по HTTPS пользователю, чтобы не давать злоумышленнику возможностей украсть или модифицировать данные перед отправкой на ваш сервер. Здесь на помощь приходит атрибут RequireHttps на соответствующих действиях контроллеров, требующий передачи данных по протоколу HTTPS.
В случаях, когда пользователя необходимо перенаправить на ту же форму по протоколу HTTPS, можно реализовать собственный фильтр, который будет автоматически осуществлять перенаправление для помеченных им действий.
public class UseHttps : ActionFilterAttribute
{
public override void OnActionExecuting(
ActionExecutingContext filterContext) {
if ( !filterContext.HttpContext.Request.IsSecureConnection) {
filterContext.Result =
new RedirectResult(
filterContext.HttpContext.Request.Url.ToString().Replace
("http:", "https:"));
filterContext.Result.ExecuteResult(filterContext);
}
base.OnActionExecuting(filterContext);
}
}
4. Корректная обработка ошибок и исключений
Ошибки и исключения в веб-приложениях случаются. В случае обращения по несуществующим адресам, либо при вызове действий контроллеров с идентификаторами несуществующих записей или некорректными идентификаторами данных сообщения об ошибках должны быть информативны для пользователя, однако не предоставлять информации о внутреннем устройстве веб-приложения. Аналогично и в случае возникновения исключений в обрабатывающем запросе коде или коде нижележащих слоев приложения. Достигается это с помощью включения Custom Errors в web.config.
Отдельно стоит обратить внимание на поведение веб-приложения при попытке неудачной загрузки файла: проверить корректность сообщений об ошибках при недостатке свободного места во временной директории на сервере, а также при загрузке чрезмерно большого файла (таймаут, ограничение на размер запроса на уровне IIS). Злоумышленник не должен меть возможность получать информацию о структуре файловой системы сервера.
5. Контроль прав доступа пользователей
При осуществлении пользователем действий, требующих определенных привилегий, необходимо проверять наличие у пользователя этих привилегий (роли, прав доступа к данным) при каждом обращении. Недостаточно проверять права доступа только при отображении основного действия со списком команд и в дальнейшем проверять лишь наличие авторизации пользователя. Совет достаточно простой, однако о нем легко забыть, пользуясь атрибутом [Authorize]. Следует проверить, что на действиях, требующих принадлежности к определенным ролям, указаны эти роли, например [Authorize(Roles = “Admin, Moderator”)].
В случаях, когда необходима более сложная логика авторизации, например на основании свойств объекта данных, может быть удобно создать собственный фильтр для авторизации. Например, если в базе данных для каждого типа данных прописан список пользователей имеющих доступ на редактирование данных, то в действии Edit может быть использован собственный фильтр AuthorizeItem, которые проверяет наличие у пользователя прав на редактирование данного конкретного объекта.
public class AuthorizeItemAttribute: FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
//...
// здесь идет код логики авторизации
// доступа к данным на основании свойств
// данных
// ...
}
Частая ошибка, которую делают разработчики в таких случаях – скрывают ссылки на редактирование объектов, не защищая сам метод, позволяющий редактировать объект.
Другой вариант решения – проверять авторизацию пользователя на уровне слоя доступа к данным, либо, для большей надежности, объединить оба этих подхода.
Почему эти советы особенно важны в контексте использования ASP.NET MVC?
В технологии ASP.NET WebForms часть из описанных выше проблем безопасности была решена за разработчика автоматически, например, WebForms брали на себя кодирование выводимого на экран текста (если не использовался Response.Write(), конечно), валидацию ViewState, валидацию параметров запросов и защиту от инъекций через валидацию механизма клиентских событий. Разумеется, для полной защиты от разных типов атак, разработчику требовалось соблюдать базовые принципы безопасности, однако во многом платформа WebForms брала заботы о безопасности на себя.
Разумеется, дополнительная защита, как и многие другие приятные полезности в WebForms, снижала гибкость решения и повышала нагрузку на систему, собственно, почему некоторые разработчики и выбирают ASP.NET MVC в погоне за большей гибкостью и большими возможностями расширения веб-приложений. В связи с этим, отказываясь от дополнительных возможностей в угоду гибкости, нужно быть готовым приложить больше усилий к тому, чтобы обезопасить веб-приложение.
Зачем ломают веб-сайты?
Часто веб-разработчики пренебрегают угрозой, считая свой сайт защищенным методом «неуловимого Джо, который совсем никому не нужен». Однако, современные хакеры в большинстве своем не веселые добряки из девяностых годов, ломающие сайты только ради взлома или дефейса (публикации ругательных надписей и забавных картинок на главных страницах сайтов). Современным хакером движет жажда наживы и его цели вовсе не так прозрачны, как «просто уронить сайт». Взломщики охотятся за базами персональной пользовательской информации, списками рассылок, возможностями осуществлять спам-рассылки от лица вашего сайта, данными учетных записей сервисов в социальных сетях, если они как-то представлены на вашем сайте. Помимо этого, цель взломщиков может быть еще более хитрой – получение информации о друзьях пользователей и их предпочтениях в музыке, кино, книгах, с целью дальнейших рассылок с возможностью «втираться» в доверие пользователей, представляясь их друзьями, что-то знающими об их вкусах.
Цели злоумышленников могут быть разные, поэтому главный принцип защиты – не дать им получить ничего, что сайт не позволяет получить анонимным пользователям. Любая информация, кажущаяся не ценной для вас, может быть ценной для ваших пользователей и может быть эксплуатирована злоумышленниками.
Как жить дальше?
Помнить об основных принципах безопасности веб-приложений и жить хорошо. В ближайшие дни я опубликую еще несколько небольших статей про самые неприятные атаки на веб-приложения и борьбу с ними.
Пожелания и комментарии по статье с радостью принимаются. Безопасность – тема сложная и всеобъемлющая, поэтому буду рад, если вы поделитесь советами в комментариях к этой статье.