Захист CSRF

Concept Core захищає змінювальні HTTP-запити від підробки між сайтами (CSRF) через синхронізаторний токен у сесії. Перевірка виконується в VerifyCsrfTokenMiddleware до контролера; токен для форм і AJAX надає CsrfTokenManager.

Див. також: Middleware → CSRF, Представлення → глобальні змінні (csrf_token).

Класи ядра

КласРоль
Concept\Core\Http\Security\CsrfTokenManager Генерація та validate() токена в сесії
VerifyCsrfTokenMiddleware Перевірка POST / PUT / PATCH / DELETE
ShareViewDataMiddleware ViewKey::CSRF_TOKEN → Twig csrf_token
Concept\Core\Http\Exceptions\Security\CsrfException 419 Page Expired

CsrfTokenManager не має окремого service provider — резолвиться автовайрингом (залежність SessionInterface).

Як це працює

  1. При першому зверненні CsrfTokenManager::getToken() генерує 64-символьний hex-токен (random_bytes(32)) і зберігає його в сесії під ключем SessionKey::CSRF_TOKEN (_csrf_token).
  2. ShareViewDataMiddleware передає токен у Twig як {{ csrf_token }}.
  3. Для POST, PUT, PATCH і DELETE middleware порівнює токен із запиту з сесійним через hash_equals().
  4. Якщо токен відсутній або не збігається — кидається CsrfException (HTTP 419 Page Expired).

Підключення middleware

У routes/web.php (або на групі маршрутів) зареєструйте перевірку після обробника валідації, але до зміни стану:

$router->lazyMiddleware(HandleValidationExceptionMiddleware::class);
$router->lazyMiddleware(VerifyCsrfTokenMiddleware::class);
$router->lazyMiddleware(StorePreviousUrlMiddleware::class);
$router->lazyMiddleware(ShareViewDataMiddleware::class);

ShareViewDataMiddleware має бути в стеку, щоб шаблони отримували актуальний токен. Сесія потрібна — реєструється через SessionServiceProvider у bootstrap/providers/app.php.

Форми (HTML)

У кожній формі, що змінює дані, додайте приховане поле з іменем _csrf_token:

У layout додайте meta для AJAX (опційно):

<meta name="csrf-token" content="{{ csrf_token }}">
<form method="post" action="{{ uri('cabinet.profile.update') }}">
    <input type="hidden" name="_csrf_token" value="{{ csrf_token }}">
    {# ... поля форми ... #}
</form>

Поле _csrf_token автоматично виключається з даних валідації в FormRequest (глобальний $globalExcept), тому воно не потрапляє в DTO.

AJAX та API з браузера

VerifyCsrfTokenMiddleware шукає токен у такому порядку:

  1. Поле _csrf_token у getParsedBody() (форма або JSON body)
  2. Заголовок X-CSRF-TOKEN (HttpHeader::X_CSRF_TOKEN)
  3. Заголовок X-XSRF-TOKEN (HttpHeader::X_XSRF_TOKEN) — через urldecode()
fetch('/api/profile', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
    },
    body: JSON.stringify(payload)
});

Для чистих зовнішніх API без сесії CSRF зазвичай не застосовують — винесіть такі маршрути в окрему групу без VerifyCsrfTokenMiddleware і використовуйте іншу автентифікацію (Bearer, API key).

Помилки та відповіді

CsrfException має код HttpStatusCode::PAGE_EXPIRED (419). Error handler (Whoops / production handler) формує HTML або JSON залежно від запиту — див. Відповіді.

Типові причини 419:

  • Сесія прострочена або відсутня cookie
  • Форма відкрита давно, токен у сесії вже змінився
  • Подвійна відправка з кешованої сторінки без актуального csrf_token

Програмний доступ

У сервісах можна отримати менеджер із контейнера:

use Concept\Core\Http\Security\CsrfTokenManager;

/** @var CsrfTokenManager $csrf */
$csrf = $container->get(CsrfTokenManager::class);
$token = $csrf->getToken();
$valid = $csrf->validate($submittedToken);

Для перевірки в тестах передавайте той самий токен, що в сесії тестового клієнта.

Захищені методи: POST, PUT, PATCH, DELETE (VerifyCsrfTokenMiddleware::PROTECTED_METHODS). GET, HEAD, OPTIONS не перевіряються — не виконуйте зміну стану через GET.