CSRF (cross-site request forgery) використовує одну властивість браузера: він додає cookie вашого домену до кожного запиту на цей домен, звідки б запит не прийшов. Людина залогінена на сайті, відкриває чужу сторінку, а там прихована форма з action="https://ваш-сайт/account/email" і автосабмітом. Браузер відправляє POST разом із cookie сесії, сервер бачить звичайний запит від автентифікованого користувача і міняє email. Прочитати відповідь зловмисник не може, але йому це й не потрібно: дія вже виконана. В OWASP Top 10:2025 CSRF віднесено до A01 Broken Access Control.
Класичний захист: секрет, якого сторонній сайт не знає. Laravel кладе випадковий токен у сесію, @csrf вставляє його в форму прихованим полем _token, а middleware порівнює його з сесійним через hash_equals(). Для JavaScript є два варіанти: заголовок X-CSRF-TOKEN з мета-тегу або заголовок X-XSRF-TOKEN, який axios сам бере з зашифрованого cookie XSRF-TOKEN. Не збіглось, отримуєте TokenMismatchException і 419 Page Expired; найчастіша чесна причина в тому, що сесія протухла, поки форма була відкрита. У Laravel 13 перевірку робить PreventRequestForgery (старі VerifyCsrfToken і ValidateCsrfToken стали застарілими аліасами), і перед токеном він дивиться на заголовок Sec-Fetch-Site: якщо браузер підтвердив same-origin, запит проходить і без токена.
Друга лінія живе в самих cookie. Laravel за замовчуванням ставить cookie сесії SameSite=Lax, і браузер не відправляє їх на крос-сайтові POST чи fetch. Але Lax пропускає верхньорівневу GET-навігацію, а middleware GET не перевіряє взагалі. Звідси правило, яке на співбесіді перевіряють через код-ревʼю: маршрут, що змінює стан, ніколи не буває GET. GET /logout чи GET /vacancies/5/delete спрацює від звичайного посилання чи редіректу з іншої сторінки, а з піддомену того ж сайту навіть від <img>, і жодні токени тут не допоможуть.
CSRF-токен не потрібен там, де облікові дані не їдуть у cookie. API, яке приймає Authorization: Bearer ..., атакувати так не вийде: сторонній сайт не може змусити браузер додати цей заголовок, тому група api в Laravel CSRF-перевірки не має. SPA на Sanctum з cookie-сесією це вже не так: там автентифікація знову в cookie, тому /sanctum/csrf-cookie і X-XSRF-TOKEN обовʼязкові. Вебхуки від платіжних систем виносять з-під CSRF, але замість токена перевіряють підпис тіла запиту. А XSS на тому самому сайті знецінює весь CSRF-захист: скрипт на сторінці прочитає токен так само, як його читає ваш фронтенд.
{{-- Форма в групі web: @csrf додає приховане поле _token з токеном сесії. --}}
<form method="POST" action="{{ route('account.destroy') }}">
@csrf
@method('DELETE')
<button>Видалити акаунт</button>
</form>
<?php
// routes/web.php: зміна стану лише через POST/PUT/PATCH/DELETE.
Route::delete('/account', [AccountController::class, 'destroy'])->name('account.destroy');
// Route::get('/account/delete', ...) відкрив би атаку через звичайне посилання.
// bootstrap/app.php, усередині withMiddleware(): вебхук без CSRF, бо його захищає підпис.
$middleware->preventRequestForgery(except: ['webhooks/payments']);
// Контролер вебхука: перевірка підпису замість CSRF-токена.
// Назву заголовка і формат підпису задає конкретний сервіс.
$expected = hash_hmac('sha256', $request->getContent(), config('services.payments.webhook_secret'));
abort_unless(hash_equals($expected, (string) $request->header('X-Signature')), 403);
Почніть із того, що CSRF існує через cookie, які браузер додає сам. Звідси випливають обидві половини відповіді: навіщо токен у формі і чому API з Bearer-токеном без нього обходиться.