В прошлом посте про AJAX был пример с configureActions() и фильтром ActionFilter\Csrf — но без подробностей, как он работает и почему CSRF в D7 касается не только запросов, которые что-то меняют. С версии главного модуля 18.0.9 (13.09.2018) AJAX-контроллер требует CSRF-токен и для GET-запросов — раньше это правило действовало только на POST.
Ещё в 2010 году на этом блоге был пост про защиту от CSRF при удалении элемента через www.site.ru/news/detail.php?ID=777&del=Y — картинкой <img src="..."> на чужом сайте можно было незаметно вызвать GET-запрос от имени авторизованного администратора. С контроллерами D7 та же логика: GET браузер выполняет сам, без подтверждения пользователя (ссылка, картинка, префетч), поэтому если под GET-действием скрывается что-то чувствительное — данные конкретного пользователя или побочный эффект — CSRF-токен нужен и там. Именно поэтому начиная с главного модуля 18.0.9 фильтр Csrf в D7-контроллерах проверяет не только POST-, но и GET-запросы.
Как устроена защита вкратце
Токен не привязан к конкретному действию — он привязан к сессии пользователя. Сервер хранит своё значение на стороне сессии, bitrix_sessid() отдаёт его клиенту (в HTML-странице или в ответе BX.ajax), а check_bitrix_sessid() при следующем запросе сравнивает пришедшее значение с тем, что лежит в сессии. Передать его можно тремя способами — параметром запроса, полем формы или заголовком X-Bitrix-Csrf-Token.
Почему это вообще защищает: сторонний сайт может заставить браузер отправить запрос на чужой домен (форма, картинка, скрипт) — куки уйдут автоматически, браузер их не разбирает по происхождению страницы. А вот прочитать токен со страницы легитимного сайта чужому сайту браузер не даёт — за это отвечает политика единого источника (same-origin policy), запрещающая одной странице читать содержимое другой. Токен просто негде подсмотреть, значит его негде подставить в поддельный запрос.
Отсюда и разница между двумя фильтрами из примеров выше: Authentication отвечает на вопрос «кто ты», Csrf — на вопрос «этот запрос действительно пришёл из интерфейса сайта, а не откуда-то ещё».
Что подключается по умолчанию
Если у action в configureActions() не указать ключ prefilters вообще — применяются фильтры по умолчанию, и их три: ActionFilter\Authentication, ActionFilter\HttpMethod (разрешает GET и POST) и ActionFilter\Csrf.
public function configureActions(): array
{
return [
'loadMore' => [], // Authentication + HttpMethod(GET, POST) + Csrf уже подключены
];
}Здесь есть тонкость из прошлого поста: пример с 'prefilters' => [new ActionFilter\Csrf()] подключает Csrf, но полностью заменяет список фильтров — вместе с ним пропадают и Authentication, и ограничение метода из HttpMethod, если явно не добавить их обратно. Для действия, которое и так открыто (данные каталога, список без привязки к пользователю), это может быть нормально. Для действия с проверкой прав — нет, и заметить пропажу Authentication по одному только configureActions() не всегда просто.
Точечная настройка вместо полной замены
Если нужно убрать один конкретный фильтр из набора по умолчанию, а остальные оставить — есть ключи +prefilters и -prefilters, которые не заменяют список, а дополняют или вычитают из него:
use Bitrix\Main\Engine\ActionFilter;
public function configureActions(): array
{
return [
'loadMore' => [
'-prefilters' => [
ActionFilter\Authentication::class,
],
],
];
}Csrf в этом случае остаётся подключённым автоматически — убирается только Authentication. Это безопаснее, чем полная замена списком prefilters, если задача — снять ровно один фильтр, а не пересобрать защиту с нуля.
Новый способ — атрибуты вместо configureActions()
В официальной документации 1С-Битрикс тот же результат теперь можно получить через PHP-атрибуты — configureActions() не переопределяется вообще, фильтры вешаются прямо на метод action:
use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Authentication;
use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\DisablePrefilters;
use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\EnablePrefilters;
#[EnablePrefilters([
new Authentication()
])]
#[DisablePrefilters([
new \Bitrix\Main\Engine\ActionFilter\Csrf()
])]
public function loadMoreAction()
{
// ...
}Смысл тот же, что у +prefilters/-prefilters из примера выше — только не массивом в configureActions(), а атрибутами на самом методе. Есть и совсем короткая форма для фильтров без параметров: #[\Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Authentication()] прямо над action.
Конструкция new Authentication() внутри аргументов атрибута работает начиная с PHP 8.1 — до этого аргументы атрибутов обязаны были быть константными выражениями. На практике это не ограничение: с 1 февраля 2026 года 1С-Битрикс требует PHP 8.2 и выше для получения обновлений, так что там, где атрибуты вообще имеет смысл проверять, PHP 8.1 уже пройден.
Оговорка: во всех официальных примерах атрибуты показаны на классе, наследующем \Bitrix\Main\Engine\Controller — том, что используется для REST-подобных маршрутов. Страница документации про компоненты (Controllerable, тот самый configureActions() из этого поста) атрибутов не показывает ни разу — только классический массив. Работает ли атрибутный синтаксис на action-методах компонента так же, как на Controller, — официально не подтверждено, стоит проверить на своём компоненте перед тем, как переносить туда боевой код.
Что приходит в ответ при ошибке
Если токен отсутствует или не совпадает, ActionFilter\Csrf возвращает ошибку с кодом invalid_csrf и сообщением Invalid csrf token. На фронтенде это response.errors с этим кодом внутри — так же, как любая другая ошибка action, разобранная в прошлом посте про AJAX.
Отдельно фильтр умеет отдавать свежий токен в заголовке ответа X-Bitrix-New-Csrf — на этом построен автоматический повтор запроса в BX.ajax.runComponentAction, о котором уже говорилось: если токен устарел, JS-клиент сам забирает новый из заголовка и повторяет вызов — на сервере для этого ничего отдельно готовить не нужно. Поэтому через BX.ajax.runComponentAction проблема с GET на практике почти не всплывает — токен подставляется независимо от метода запроса.
Всплывает она там, где GET собирают вручную — свой fetch или прямая ссылка на экшен без BX.ajax:
// fetch без method — это GET; без sessid упадёт с invalid_csrf
fetch(`/bitrix/services/main/ajax.php?action=vendor:example.getStats`)
.then((r) => r.json())
.then((data) => console.log(data));
// {"status":"error","errors":[{"code":"invalid_csrf","message":"Invalid csrf token"}]}Исправление — добавить sessid в параметры запроса тем же способом, что и для POST из прошлого поста:
fetch(`/bitrix/services/main/ajax.php?action=vendor:example.getStats&sessid=${BX.bitrix_sessid()}`)
.then((r) => r.json())
.then((data) => console.log(data));Когда действительно можно убрать Csrf
Если действие на GET читает данные, одинаковые для любого посетителя, без привязки к текущему пользователю и без побочных эффектов — снять Csrf тем же способом, что и Authentication выше, оправдано:
public function configureActions(): array
{
return [
'getPublicStats' => [
'-prefilters' => [
ActionFilter\Authentication::class,
ActionFilter\Csrf::class,
],
],
];
}Но если под GET-действием прячется что-то из данных пользователя (даже просто "сколько товаров у меня в корзине") или оно вызывает побочный эффект (счётчик, лог, изменение состояния) — снимать Csrf не стоит: это тот самый случай с картинкой <img src="..."> из старого поста, просто на новом API.
Независимо от метода: если действие меняет данные — это POST, а не GET. Csrf на GET закрывает конкретно случайные и чувствительные к CSRF действия, спрятанные под безобидным на вид чтением, а не отменяет это правило.
