Защита от CSRF атак в ASP.NET Core приложении

CSRF ASP.NET Core Docker

В этом посте я хочу рассказать о том, что такое CSRF (Cross-Site Request Forgery) атака и как от нее защититься.

Акт 1

Представим себе сайт интернет банкинга какого-нибудь выдуманного банка. Клиент этого банка вполне легальным образом логинится на сайте, выполняет ряд операций и закрывает страницу не сделав выход. После этой операции в браузере клиента осталась кука.

Клиент обманным путем попадает на сайт мошенников, которые отправляют AJAX запрос на сайт интернет банкинга с запросом о переводе некой суммы денег на счет мошенников. Так как в браузере осталась кука с сайта интернет банкинга, то она любезно прикрепляется браузером к запросу мошенников. Всё.

Акт 2

Теперь попытаемся понять, что произошло.

Т.к. запрос отправляется на другой домен, и мы не рассматриваем ситуацию XSS инъекции на сайте интернет банкинга, то браузер, по умолчанию (если не настроен CORS), не даст прочитать ответ от сервера. А нашим мошенникам это по сути и не надо. Важно, что сам запрос был отправлен на сервер и обработан. Т.е. произошло списание денежных средств. Атаку можно считать успешно выполненной.

Акт 3

Перейдем к защите.

Для защиты от этой атаки используется 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. Но, это уже другая история...