<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
БЕЗПЕКА · JUNIOR ЧАСТО ПИТАЮТЬ

Що таке CSRF, як Laravel від нього захищає і коли CSRF-токен не потрібен?

CSRF змушує браузер залогіненої людини відправити запит на ваш сайт зі сторінки зловмисника: cookie сесії браузер додає сам, і сервер бачить легітимного користувача. Laravel перевіряє кожен POST, PUT, PATCH і DELETE у групі `web`: запит проходить, якщо `Sec-Fetch-Site` каже `same-origin` або якщо токен з `_token`, `X-CSRF-TOKEN` чи `X-XSRF-TOKEN` збігається з токеном сесії, інакше 419. Токен не потрібен там, де автентифікація не їде в cookie, наприклад з Bearer-токеном у заголовку.

Навіщо в кожній формі `@csrf` і що станеться, якщо його прибрати?
Користувач бачить «419 Page Expired» після відправки форми. Що це означає і де шукати?
Чому для API з Bearer-токеном CSRF-токен не потрібен, а для SPA на Sanctum з cookie потрібен?
У нас є маршрут `GET /account/delete`. Що з ним не так, якщо CSRF-захист увімкнено?
CSRF SameSite 419 PreventRequestForgery Sanctum cookies

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);
Що корінь проблеми в автоматичній відправці cookie: атака працює лише там, де браузер сам додає облікові дані.
Що токен привʼязаний до сесії, вбудований у сторінку і невідомий сторонньому сайту, а порівнюється через `hash_equals()`.
Що в Laravel 13 перевірку робить `PreventRequestForgery` (старі `VerifyCsrfToken` і `ValidateCsrfToken` лишились як застарілі аліаси), і він спершу дивиться на `Sec-Fetch-Site`, а вже потім на токен.
Що `SameSite=Lax` за замовчуванням закриває більшість крос-сайтових POST, але не GET-навігацію, тому GET ніколи не змінює стан.
Що API з токеном у заголовку `Authorization` CSRF не потребує, а SPA з cookie-сесією через Sanctum потребує.
Виключити маршрут через `except`, щоб «прибрати 419», без іншої перевірки: для вебхука її замінює підпис запиту, для звичайної форми нічого.
Робити зміну стану через GET (`/logout`, `/account/delete`, `/vote?id=5`): GET не перевіряється взагалі, а `SameSite=Lax` пропускає cookie на переході за посиланням, тож звичайне посилання чи редірект з чужої сторінки виконає дію.
Вважати, що CORS захищає від CSRF: CORS вирішує, чи може сторінка прочитати відповідь, а простий POST з форми доходить до сервера і виконується.
Думати, що JSON-ендпоїнт у групі `web` не потребує токена, бо «форма JSON не відправить»: перевірка в Laravel все одно стоїть, і прибирати її з цієї причини не можна.
Плутати CSRF з XSS: якщо на сайті є XSS, скрипт прочитає токен зі сторінки, і CSRF-захист уже нічого не дає.
ПОРАДА

Почніть із того, що CSRF існує через cookie, які браузер додає сам. Звідси випливають обидві половини відповіді: навіщо токен у формі і чому API з Bearer-токеном без нього обходиться.

оновлено 8 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

CSRF працює тому, що браузер сам додає cookie до запиту на ваш домен. Заголовок `Authorization` сторонній сайт додати не може, тож підробити автентифікований запит неможливо. CORS керує читанням відповіді, а простий POST з форми доходить до сервера. `PreventRequestForgery` пропускає GET, HEAD і OPTIONS без перевірки. `Lax` відправляє cookie на верхньорівневу GET-навігацію, саме тому GET не має змінювати стан.