В этом посте я хочу рассказать о том, что такое CSRF (Cross-Site Request Forgery) атака и как от нее защититься.
Представим себе сайт интернет банкинга какого-нибудь выдуманного банка. Клиент этого банка вполне легальным образом логинится на сайте, выполняет ряд операций и закрывает страницу не сделав выход. После этой операции в браузере клиента осталась кука.
Клиент обманным путем попадает на сайт мошенников, которые отправляют AJAX запрос на сайт интернет банкинга с запросом о переводе некой суммы денег на счет мошенников. Так как в браузере осталась кука с сайта интернет банкинга, то она любезно прикрепляется браузером к запросу мошенников. Всё.
Теперь попытаемся понять, что произошло.
Т.к. запрос отправляется на другой домен, и мы не рассматриваем ситуацию XSS инъекции на сайте интернет банкинга, то браузер, по умолчанию (если не настроен CORS), не даст прочитать ответ от сервера. А нашим мошенникам это по сути и не надо. Важно, что сам запрос был отправлен на сервер и обработан. Т.е. произошло списание денежных средств. Атаку можно считать успешно выполненной.
Перейдем к защите.
Для защиты от этой атаки используется 2 AntiForgery-токена. Один отправляется на клиент в виде куки, а второй в скрытом поле в форме. Когда клиент самостоятельно выполняет запрос на сайте интернет банкинга, то на сервер уходят 2 токена. Один в форме или в заголовке, второй в куке. Сервер проверяет их валидность и с случае неладного возвращает ответ со статусом 400.
Для начала изменим класс Startup
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddMvc(options =>
{
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
}
}
ASP.NET MVC по умолчанию в каждую форму добавляет скрытое поле такого вида:
<input name="__RequestVerificationToken" type="hidden" value="CfEJ6IxjsTbAPh1AhIp.........5PEbMY4s-2ALCzGETVQe6VixWhh3WMMXzVSXJ3w">
и создает куку с именем вида .AspNetCore.Antiforgery.leVP-PGSx2U
Этого достаточно для защиты форм созданных ASP.NET MVC Core. А как быть с AJAX запросами?
Все просто. Добавим в файл _Layout.cshtml
такой кусок кода:
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
public string GetAntiXsrfRequestToken()
{
return Xsrf.GetAndStoreTokens(Context).RequestToken;
}
}
<meta name="requests-validation-token" content="@GetAntiXsrfRequestToken()" />
Теперь модифицируем скрипт отправки запроса на сервер:
function getRequestsValidationToken(): string {
const key = "requests-validation-token";
const elements = document.getElementsByTagName("meta");
for (let i = 0; i < elements.length; i++) {
const element = elements.item(i);
if (element.name == key) {
return element.content;
}
}
return null;
}
const token = getRequestsValidationToken();
if (token == null) {
throw new Error("Can't read requests validation token");
}
$.ajax({
type: 'POST',
url: url,
headers: {
"RequestVerificationToken": token,
}
//OR
//beforeSend: function(xhr) {
// xhr.setRequestHeader("RequestVerificationToken", token);
//}
}).done(function(data) {
alert(data);
});
Внимательный читатель сейчас может задать вопрос: если сам токен находится в теле страницы, то, что мешает злоумышленникам сделать GET
запрос нашей страницы и прочитать этот токен? Может, но браузер скажет: Failed to load https://www.vitaliy.org/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://hacker-site.com' is therefore not allowed access
.
Если сайт работает на нескольких серверах (в ферме или в докере), то для того, чтобы один сервер смог расшифровать токен, созданный другим сервером, необхоидмо настроить DataProtection
. В моем случае это сделано с помощью шаринга ключа шифорвания в Redis.
using Microsoft.AspNetCore.DataProtection;
public void ConfigureServices(IServiceCollection services)
{
var redis = ConnectionMultiplexer.Connect(Configuration.GetConnectionString("RedisConnection"));
services.AddDataProtection().PersistKeysToRedis(redis, Configuration["Services:Redis:Key"]);
}
PS: что еще надо сделать? Настроить Content-Security-Policy
и бороться с XSS
. Но, это уже другая история...