Защита операций с cookies
Аутентификация cookies удобна тем, что браузер прикладывает билет автоматически. Эта же автоматичность создаёт риск CSRF: чужая страница может попытаться отправить изменяющий запрос с уже имеющейся сессией пользователя. Сервер должен отличать действие, подготовленное его интерфейсом, от навязанной отправки. В предыдущих HTTPS-ветках проверка уже была включена; теперь разберём её механизм и вынесем повторяющийся код в endpoint filter.
lesson-23/after архива продолжения продолжает after урока 22. Login, logout и редакторская JSON-команда сохраняют проверку. Изменение состоит в общей точке применения, а не в исправлении намеренно оставленного незащищённого входа. Сервер, браузерные отправки и тесты при подготовке не выполнялись. Все описанные статусы предполагают действительный сертификат и HTTPS-песочницу на localhost.
Две части token выполняют одну проверку
Antiforgery использует связанную пару: служебный token в cookie и request token, который доверенный интерфейс получает и передаёт отдельно. В нашей конфигурации второй приходит в X-CSRF-TOKEN. Чужой сайт не должен уметь прочитать этот ответ с доверенного origin и получить подходящую пару. Само знание адреса endpoint не даёт нужного token.
Регистрация задаёт имя заголовка и защищённую cookie:
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
options.Cookie.Name = "CatalogLab.Antiforgery";
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
});
GET /api/csrf вызывает GetAndStoreTokens и возвращает request token в JSON. Его ответ получает Cache-Control: no-store: это данные конкретного браузерного состояния, их нельзя раздавать как общий публичный кеш. Служебная cookie не становится обычной cookie аутентификации; наличие token ещё не означает, что пользователь вошёл или имеет право менять JavaScript.
После успешного login principal меняется. Клиент должен получить новую пару request token для новой личности. После logout прежний аутентифицированный token тоже не является универсальным token анонимного состояния. В учебной последовательности запрос на получение пары повторяется после каждой смены личности, что предотвращает использование старого контекста.
Фильтр охватывает изменяющие JSON-endpoints
Полный CsrfFilter выполняет проверку перед предметным handler:
public sealed class CsrfFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var antiforgery = context.HttpContext.RequestServices
.GetRequiredService<IAntiforgery>();
try { await antiforgery.ValidateRequestAsync(context.HttpContext); }
catch (AntiforgeryValidationException)
{
return Results.Problem(statusCode: 400,
title: "Недействительный token операции");
}
return await next(context);
}
}
Фильтр прикрепляется явно к login, logout и rename. Он не применяется ко всем GET, потому что безопасные чтения не должны изменять состояние. Отдельный endpoint выдачи token остаётся доступным и анонимному браузеру, чтобы тот мог защитить login. Если защищать только уже аутентифицированные команды, вход остался бы отдельной незакрытой границей.
Валидация имеет конкретный смысл: проверяется криптографически связанная пара и текущая идентичность. Это не буквальное доказательство адреса страницы, на которой человек нажал кнопку, и не подтверждение его намерения. Token, украденный через XSS на доверенном origin, способен пройти эту проверку. Поэтому вывод «CSRF включён, значит произвольный JavaScript безопасен» неверен.
Фильтр возвращает 400 при неподходящей паре. После отказа он не вызывает next, поэтому rename не меняет название и login не выпускает сессию. Отсутствие token и случайная строка в заголовке дают одинаковый предметный отказ. Исключение не превращается в общую 500, как если бы ожидаемую ошибку CSRF никто не обработал.
SameSite, origin и CORS не подменяют token
SameSite опирается на понятие site, которое отличается от origin. Два сервиса могут относиться к одному site и всё же иметь разные origin. Cookies также не разделяются по порту. Поэтому локальный адрес на соседнем порту не доказывает полную изоляцию сессии. В примере не используются широкие Domain cookies и не разрешается произвольный cross-origin frontend.
CORS определяет, может ли браузерный код прочитать cross-origin ответ и выполнить некоторые виды отправок. Он не является авторизацией пользователя и не должен рассматриваться как единственная защита изменяющей команды. Обычная HTML-форма может отправить запрос даже тогда, когда вызывающая страница не может прочитать ответ. Серверу нужен собственный отказ до изменения данных.
Проверка Origin или Referer бывает дополнительной политикой, но требует определения доверенных адресов и поведения при отсутствии заголовка. В этой ветке она не реализована и не объявляется состоявшейся. Antiforgery token обеспечивает выбранный механизм для cookie-команд; если продукт добавит внешние клиентов, потребуется отдельное решение их аутентификации и контрактов.
Нельзя превращать GET /api/courses/javascript?delete=true в изменяющую операцию ради удобной ссылки. Браузеры, предварительная загрузка, поисковые роботы и чужие страницы могут инициировать безопасные запросы. Разделение методов — часть защиты: чтение не должно выпускать новый аккаунт, выполнять logout, переименовывать объект или удалять файл.
Проверяемое различие запросов
Для будущего самостоятельного сценария сначала получите token анонимного состояния и войдите, затем обновите token. Отправьте rename собственного JavaScript с cookie сессии, но без X-CSRF-TOKEN: ожидается 400, прежнее название и прежняя версия. Случайное значение также даёт 400. Действительная пара допускает переход к ресурсной авторизации; успешное изменение возвращает новую версию.
Теперь используйте действительный token для чужого Markdown. CSRF пройдёт, но политика вернёт 403. Это полезное различие: token не даёт право на ресурс. Для неаутентифицированной защищённой команды общая политика может остановить запрос на 401 раньше фильтра. Порядок отказов определяется конкретными границами, а не обязательным единым статусом всех неверных отправок.
Учебная форма в архиве не включена; request token предназначен для будущего интерфейса либо самостоятельного HTTP-клиента, который сохраняет cookies. При реальном браузерном сценарии нужно проверить обновление token, несколько вкладок, истечение билета и отказ без изменения. Здесь сохранён исходный контракт и добавлена общая точка проверки, а фактическая работа ещё требует исполнения после редакционного этапа.
Механизм и границы описаны в документации antiforgery ASP.NET Core 10. Использование фильтра для наших JSON-команд — собственное решение маленького приложения, а не утверждение об автоматической защите любого Minimal API.
В нескольких вкладках каждый token должен соответствовать текущей личности браузера. Если одна вкладка выполнила logout и новый login, другая может продолжать показывать старый интерфейс и хранить прежний request token. Отказ в такой ситуации не нужно обходить отключением validation. Интерфейс заново читает состояние пользователя и получает подходящий token, после чего человек подтверждает действие в актуальном контексте. Нельзя автоматически повторять любую изменяющую команду при ошибке: повтор допустим лишь после понимания исхода первой попытки. Здесь отказ фильтра происходит до предметного handler, поэтому новая попытка с исправленной парой не скрывает уже выполненный rename. Ошибки после начала самой операции требуют иной модели восстановления.