Веб платформа ASP.NET за последние десятилетия получила достаточно широкое распространение. С ее помощью разрабатывают веб-сайты и веб-приложения с помощью таких средств как HTML, CSS и JavaScript. Также с помощью ASP.NET можно создавать веб-API и веб сокеты.
Одной из основных проблем при разработке в контексте безопасности является невозможность функциональной проверки у того, или иного приложения. То есть, при разработке приложения мы не можем просто подключить какие-то библиотеки, сделать какие-то настройки при сборке проекта, и затем сказать, что все, наше приложение защищено и можно передавать его в продакшен. Так не бывает. На практике возможны два основных сценария: худший когда мы узнаем о проблемах с безопасностью нашего приложения из СМИ и интернета, например, когда приложение взломали или, когда в сеть утекли данные, обрабатываемые нашим приложением (да, обычно эти утечки организуют бывшие или действующие сотрудники, и это не совсем вина приложения, но, как говориться осадочек все-равно остается). Более мягким является сценарий, когда мы узнаем об уязвимостях в нашем приложении в результате пентеста или когда сознательные исследователи сами сообщают разработчикам о найденных уязвимостях, например в рамках баг баунти. В таком случае нас скорее всего еще не скомпрометировали и репутационного ущерба для компании разработчика не будет. Ну а срочные баг фиксы выпускают все.
И к чему было все это вступление? ASP.NET является достаточно сложным решением, а сложность это всегда враг безопасности. С одной стороны вроде-бы злоумышленнику трудно разобраться в сложном решении, но с другой и защищать сложное решение тоже весьма непросто. И практика показывает, что очень часто именно злоумышленники быстрее разбираются в сложных системах и оперативно находят уязвимые места.
Так и фреймворк .NET Майкрософт разработала ряд “готовых” к использованию компонентов безопасности, которые обеспечивают выполнение общих требований по безопасности веб приложений, например это управление сеансами, аутентификация пользователей, хранение учетных данных и т.д. Большинство этих защитных механизмов включены в работу платформы .NET по умолчанию. И таким образом и разработчики и атакующие могут легко получить доступ к этим функциям. В результате использование этих функций делает работу всей платформы более уязвимой.
И далее в этой статье мы основные угрозы и компоненты, которые необходимо использовать при разработке веб приложений.
Ролевая модель безопасности
Начнем с основных концепций безопасности, которые есть в .NET. Прежде всего это ролевая модель безопасности. Ролевая модель безопасности подразумевает два основных режима работы. Первый это создание пользователей и ролей, которые не зависят от ролей ОС Windows. Такая модель удобна, когда все разграничение прав внутри приложения ведется именно с помощью ролей. Все это никоим образом не связано (и в принципы не должно быть связано) с учетками в ОС. Например, это доступность каких-либо компонентов веб приложения в зависимости от заданной роли пользователя.
Второй предполагает жесткую привязку ролей в приложении к учетным записям в Windows. Обычно подобную модель безопасности можно встретить в веб-приложениях, работающих во внутрикорпоративной среде и тесно связанных с инфраструктурой Active Directory.
Аутентификация в ASP.NET приложениях обычно реализуется или с помощью аутентификации Windows или с помощью форм. Первый вариант построен на использовании штатных средств операционной системы . В каждом случае пользователь предъявляет некий аналог “удостоверения” – в первом случае это SID (Security Identifier), а во втором случае формируется так называемый билет, который затем сохраняется в cookie. Далее мы подробно рассмотрим именно второй вариант, так как он наиболее распространен в веб приложениях и наиболее интересен с точки зрения возможных уязвимостей.
Аутентификация с помощью форм
Когда клиент заходит на страницу и вводит логин и пароль, его учетные данные передаются на сервер. В случае, если аутентификация успешно пройдена, сервер формирует сессионный билет для данного пользователя и при всех последующих обращениях к серверу используется уже этот билет, данные которого сохраняются в cookie. По умолчанию время жизни билета составляет 30 минут. По окончании этого времени билет может быть автоматически продлен. В случае, если в течении определенного интервала времени обращений больше не было, сервер отправляет клиенту сообщение с предложением “забыть” этот билет, в результате чего браузер удаляет данный куки и пользователю надо заново вводить учетные данные, чтобы войти в систему.
Казалось бы, все вполне логично и правильно. Но клиент (а точнее злоумышленник) может периодически просить сервер продлевать свой билет, по сути, неограниченное число раз. То есть сервер никак не контролирует, какое количество раз продлевался билет. И злоумышленник может просто игнорировать полученное от сервера предложение “забыть” свой билет и продолжать продлевать его и дальше.
Таким образом, если злоумышленник смог похитить куки с билетом, он сможет использовать пользовательскую сессию.
Нам необходимо как-то контролировать сессии для нашего веб приложения. Для этого мы можем осуществлять идентификацию и проверку информации о клиенте от запроса к запросу. Если запрос поступает от другого клиента для одного и того же идентификатора сеанса, то это можно рассматривать как атаку. Чтобы идентифицировать клиента, ниже приведена некоторая информация, которую мы можем получить из запроса (ip-адрес, идентификационная информация пользователя и информация о браузере и ОС).
По отдельности каждый из этих параметров может быть в той или иной степени изменен злоумышленником, но вместе они могут помочь защитить сессию. Если файл cookie содержит эту информацию также помимо идентификатора сеанса или идентификатор сеанса генерируется таким образом, что эти данные сохраняются в этом идентификаторе, то для каждого запроса, поступающего от клиента, следует проверять, и если данные сеанса и данные запроса не совпадают, то это попытка захвата сеанса, и мы можем заблокировать этот запрос.
Этот метод может быть реализован в ASP.NET очень простым способом. Давайте рассмотрим, как можно использовать идентификатор сеанса для сохранения информации о проверке. Простой идентификатор сеанса, сгенерированный ASP.NET выглядит как fvd4hu45ihqco1ftmvprfe69
буквенно-цифровое значение длиной 24 символа. Итак, теперь давайте попробуем привязать данные клиента к этому идентификатору сеанса.
Для этого необходимо взять ASP.NET_SessionID, куки и значение, создать на основе данных о браузере, ОС, URL с которого перешли и других параметров уникальный хэш, присоединить его к значению куки и подключить этот куки к ответу. Для проверки мы соответственно берем полученные данные, берем хэш и сравниваем эти значения.
Эти проверки могут быть на уровне модуля HTTP или вы можете разместить в файле Global.asax.
Это фрагмент кода генерирует хэши на основе пользовательской информации.
string GenerateHashKey()
{
StringBuilder myStr = new StringBuilder();
myStr.Append(Request.Browser.Browser);
myStr.Append(Request.Browser.Platform);
myStr.Append(Request.Browser.MajorVersion);
myStr.Append(Request.Browser.MinorVersion);
myStr.Append(Request.LogonUserIdentity.User.Value);
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] hashdata = sha.ComputeHash(Encoding.UTF8.GetBytes(myStr.ToString()));
return Convert.ToBase64String(hashdata);
}
Как видите мы берем набор значений о браузере и пользователе и создаем на их основе хэш.
Посмотрим, как можно проверить полученный запрос.
protected void Application_BeginRequest(object sender, EventArgs e)
{
if (Request.Cookies["ASP.NET_SessionId"] != null && Request.Cookies["ASP.NET_SessionId"].Value != null)
{
string newSessionID = Request.Cookies["ASP.NET_SessionID"].Value;
if (newSessionID.Length <= 24)
{
//Log the attack details here
throw new HttpException("Invalid Request");
}
if (GenerateHashKey() != newSessionID.Substring(24))
{
//Log the attack details here
throw new HttpException("Invalid Request");
}
//Use the default one so application will work as usual//ASP.NET_SessionId
Request.Cookies["ASP.NET_SessionId"].Value = Request.Cookies["ASP.NET_SessionId"].Value.Substring(0, 24);
}
}
А эта процедура обновляет значение сессионных куков.
protected void Application_EndRequest(object sender, EventArgs e)
{
if (Response.Cookies["ASP.NET_SessionId"] != null)
{
Response.Cookies["ASP.NET_SessionId"].Value = Request.Cookies["ASP.NET_SessionId"].Value + GenerateHashKey();
}
}
Таким способом в ASP.NET можно контролировать пользовательские сессии и отслеживать возможные манипуляции с ними.
Cross Site Request Forgery
Старый добрый CSRF актуален и для .NET. Напомним, что CSRF это вид атак на посетителей веб-сайтов, использующий недостатки протокола HTTP. Если жертва заходит на сайт, созданный злоумышленником, от её лица тайно отправляется запрос на другой сервер (например, на сервер платёжной системы), осуществляющий некую вредоносную операцию (например, перевод денег на счёт злоумышленника).
Для осуществления этой атаки должны быть выполнено несколько условий. Во-первых, жертва должна быть аутентифицирована на том сервере, на котором выполняется запрос. Также этот запрос не должен требовать какого-либо подтверждения со стороны пользователя, которое не может быть автоматизировано, то есть, которое не может быть подделано атакующим скриптом.
В качестве примера рассмотрим ситуацию, когда пользователь входит в систему www.bank.com с помощью проверки подлинности на основе тех самых форм, о которых мы говорили чуть раньше. Сервер выполняет проверку подлинности пользователя и выдает ответ, включающий билет пользователя в cookie. При этом, сайт банка доверяет любому запросу, который он получает с допустимым билетом в cookie. Далее пользователь не завершая принудительно сессию на сайте банка посещает другой сайт. www.hacker.com который содержит HTML-форму, в которой запрятана отправка в форме следующих даных:
<form action="https://bank.com/api/account" method="post">
<input type="hidden" name="Transaction" value="withdraw" />
<input type="hidden" name="Amount" value="1000000" />
<input type="submit" value="Click to collect your prize!" />
</form>
Обратите внимание, что форма отправит данные именно на сайт банка. Это "межсайтовая" часть CSRF. Далее под каким-либо предлогом на hacker.com пользователю предлагается нажать кнопку, которая скрытно отправит данные формы. При отправке формы браузер автоматически отправит и билет в cookie для запрошенного домена www.bank.com, ведь пользователь формально еще залогинен на сайте банка. В результате запрос будет выполнен на сайте банка. Стоит отметить, что использование HTTPS не предотвращает атаки CSRF. Вредоносный сайт может отправлять https://www.bank.com/ запрос так же легко, как и небезопасный запрос.
Такие атаки становятся возможными благодаря тому, что в браузере хранятся cookie, выданные приложением, эти куки содержат в том числе сеансовые билеты, прошедших проверку пользователей, и наконец браузер отправляет веб-приложению все cookie связанные с доменом независимо от того, как запрос к приложению был создан в браузере.
Как бороться
Общие рекомендации OWASP для борьбы с CSRF требуют включения непредсказуемого токена в каждый HTTP-запрос. Лучше всего, когда этот уникальный токен помещен в скрытое поле. В определенной степени это метод аналогичен тому, что мы уже использовали с контролем сессий.
В ASP.NET эти рекомендации реализуются следующим образом. Маркер защиты от подделки, представленный в cookie, сгенерированный как псевдослучайное значение и зашифрованный.
Дополнительный токен, включаемый либо в виде поля формы, заголовка, либо в виде того же cookie. Он включает в себя то же псевдослучайное значение плюс дополнительные данные из идентификатора текущего пользователя и также шифруется.
Эти токены будут сгенерированы на стороне сервера и переданы вместе с html-документом в браузер пользователя. Маркер cookie будет включаться по умолчанию всякий раз, когда браузер отправляет новый запрос, в то время как приложению необходимо убедиться, что маркер запроса также включен.
В случае, если значения этих двух токенов отличаются, либо одно из них отсутствует, запрос на подключение будет отклонен.
Чтобы проверка защиты от подделки прошла успешно, нам нужно убедиться, что как токен cookie, так и токен запроса включены в запросы, которые будут проходить проверку.
<form asp-controller="Foo" asp-action="Bar">
…
<button type="submit">Submit</button>
</form>
Сгенерируется следующий html:
<form action="/Foo/Bar" method="post">
…
<button type="submit">Submit</button>
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8P4n6uxULApNkzyVaa34lxdNGtmIOsdcJ7SYtZiwwTeX9DUiCWhGIndYmXAfTqW0U3sdSpzJ-NMEoQPjxXvx6-1V-5sAonTik5oN9Yd1hej6LmP1XcwnoiQJ2dRAMyhOMIYqbduDdRI1Uxqfd0GszvI">
</form>
Content Security Policy
Еще одна широко известная атака это межсайтовый скриптинг (XSS). Эти атаки реализуются на стороне клиента, но мы можем предотвратить подобные атаки с помощью политик безопасности контента (Content Security Policy). Политика безопасности контента, является средством, с помощью которого веб-страницы могут контролировать, какие ресурсы разрешено загружать. Например, страница может явно объявлять домены, с которых разрешено загружать ресурсы JavaScript, CSS, изображения и т.д.
В современных веб приложениях данная политика содержит множество различных параметров: она сообщает браузеру, что он может загружать фреймы, Ajax-запросы, веб-сокеты, шрифты, изображения, аудио, видео, и другие компоненты. Вполне возможно, что вы не используете большинство вещей из этого списка. Гораздо лучшей политикой было бы заблокировать все по умолчанию, а затем разрешить только определенные ресурсы, которые вы действительно используете, как показано ниже.
Content-Security-Policy: default-src 'none';
script-src TrustedSite.com;
style-src 'self';
img-src 'self';
font-src 'self';
connect-src 'self';
form-action 'self'
Здесь мы разрешаем загрузку скриптов с доверенного сайта, а другие компоненты, такие как стили, шрифты и прочее могут грузится только с нашего сайта.
Далее посмотрим фрагмент кода ASP.NET для конфигурации настройки CSP. Здесь мы добавляем параметр Content-Security-Policy со значением, позволяющим загрузку контента только с исходного сайта.
<system.webServer>
…
<httpProtocol>
<customHeaders>
<add name="Content-Security-Policy" value="default-src 'self';" />
</customHeaders>
</httpProtocol>
…
</system.webServer>
Чтобы не закрыть лишнего
Стоит понимать, что CSP может нанести вред при неправильной настройке. Так, если функционал вашего сайта предполагает демонстрацию видео с Youtube, а CSP не позволяет использовать этот ресурс получится не слишком хорошо.
Чтобы справиться с этой проблемой, W3C создал HTTP-заголовок Content-Security-Policy-Report-Only. Это работает точно так же, как Content-Security-Policy, но оно только ничего не блокирует, а просто сообщает о попытках открыть нарушения ваших политик.
Заключение
В этой статье мы коснулись некоторых аспектов безопасности Web применительно к приложениям написанным на ASP.NET, хотя большинство из этих атак также актуальны для приложений, написанных на других языках.
А прямо сейчас приглашаю всех читателей на talk-сессию: "Мифы и реальность в DevSecOps".