Nội dung bài viết
- 1.CSRF hoạt động như thế nào?
- 2.Laravel chống CSRF bằng cơ chế nào?
- 3.Cách dùng CSRF token trong Laravel
- 3.1Form HTML: dùng @csrf
- 3.2AJAX: gửi header X-CSRF-TOKEN
- 3.3Axios và SPA: cookie XSRF-TOKEN
- 4.Loại trừ route khỏi kiểm tra CSRF
- 5.Lỗi 419 Page Expired: nguyên nhân và cách sửa
- 6.CSRF token có đủ để bảo vệ ứng dụng?
- 7.Kinh nghiệm từ Hozitech
CSRF (Cross-Site Request Forgery) là kiểu tấn công lừa trình duyệt của người dùng đang đăng nhập gửi một request ghi dữ liệu (đổi email, chuyển tiền, xóa bài) tới ứng dụng mà họ không hề chủ động. Laravel chặn CSRF mặc định cho mọi route trong nhóm web: bạn chỉ cần thêm @csrf vào form và gửi header X-CSRF-TOKEN khi gọi AJAX.
Bài này giải thích cơ chế tấn công, cách Laravel 13 (bản hiện hành, yêu cầu PHP 8.3+) kiểm tra request, cách dùng token trong form, AJAX, SPA, cách loại trừ route webhook và cách xử lý lỗi 419 hay gặp.

CSRF hoạt động như thế nào?
Trình duyệt tự gửi kèm cookie phiên đăng nhập với mọi request tới một tên miền, kể cả khi request đó xuất phát từ trang web khác. Kẻ tấn công lợi dụng đúng điểm này. Giả sử ứng dụng của bạn có route POST /user/email để đổi email tài khoản. Một trang độc hại chỉ cần chứa form tự gửi:
<form action="https://ung-dung-cua-ban.vn/user/email" method="POST">
<input type="email" name="email" value="[email protected]">
</form>
<script>
document.forms[0].submit();
</script>
Nếu nạn nhân đang đăng nhập và mở trang đó, trình duyệt gửi request kèm cookie hợp lệ, server tưởng chính người dùng yêu cầu đổi email. Kẻ tấn công không đọc được phản hồi, nhưng không cần đọc: hành động đã xảy ra.
Cách phòng chống là yêu cầu mỗi request POST, PUT, PATCH, DELETE phải mang một giá trị bí mật gắn với phiên làm việc mà trang khác không thể đọc được. Đó chính là CSRF token.
Laravel chống CSRF bằng cơ chế nào?
Bảo vệ CSRF nằm trong một middleware thuộc nhóm web, được áp tự động cho mọi route trong routes/web.php. Tên middleware khác nhau theo phiên bản:
| Phiên bản | Middleware | Nơi cấu hình |
|---|---|---|
| Laravel 13 | PreventRequestForgery (kiểm tra nguồn gốc rồi mới tới token) | bootstrap/app.php, phương thức preventRequestForgery() |
| Laravel 11, 12 | ValidateCsrfToken | bootstrap/app.php, phương thức validateCsrfTokens() |
| Laravel 10 trở về trước | App\Http\Middleware\VerifyCsrfToken | Thuộc tính $except trong chính class đó |
Khi nâng cấp lên Laravel 13, VerifyCsrfToken và ValidateCsrfToken vẫn chạy dưới dạng alias đã lỗi thời (deprecated); nên đổi các chỗ tham chiếu trực tiếp, ví dụ withoutMiddleware([...]) trong test, sang PreventRequestForgery.
Ở Laravel 13, middleware làm hai lớp kiểm tra:
- Kiểm tra nguồn gốc: trình duyệt hiện đại gửi header
Sec-Fetch-Sitecho biết request đến từ cùng origin, cùng site hay site khác. Nếu là cùng origin, request được cho qua ngay. Header này chỉ có trên kết nối HTTPS. - So khớp token: nếu không xác minh được nguồn gốc (trình duyệt cũ, không có HTTPS…), middleware quay về cách truyền thống: so token gửi lên với token lưu trong session.
Token được sinh theo phiên (session), không đổi theo từng request. Nó chỉ đổi khi session được tạo lại, ví dụ sau khi đăng nhập. Vì vậy mở nhiều tab cùng lúc vẫn gửi form bình thường. Bạn lấy token hiện tại bằng:
use Illuminate\Http\Request;
Route::get('/token', function (Request $request) {
$token = $request->session()->token();
// hoặc dùng helper
$token = csrf_token();
});
Xem thêm cách session và cookie lưu trạng thái đăng nhập trong bài phân biệt session và cookie.
Cách dùng CSRF token trong Laravel
Form HTML: dùng @csrf
Mọi form có method POST, PUT, PATCH hoặc DELETE cần một trường ẩn _token. Directive @csrf của Blade sinh sẵn trường này:
<form method="POST" action="/profile">
@csrf
@method('PUT')
<input type="text" name="name" value="{{ old('name') }}">
<button type="submit">Lưu</button>
</form>
{{-- @csrf tương đương với: --}}
<input type="hidden" name="_token" value="{{ csrf_token() }}">
Form dùng @method('PUT') vẫn phải có @csrf, vì về bản chất trình duyệt vẫn gửi POST. Cú pháp Blade chi tiết có trong bài view và Blade template trong Laravel.
AJAX: gửi header X-CSRF-TOKEN
Middleware cũng đọc token từ header X-CSRF-TOKEN. Cách thường dùng là đặt token vào thẻ meta trong layout rồi để JavaScript đọc ra:
<meta name="csrf-token" content="{{ csrf_token() }}">
fetch('/posts/15/like', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
},
});
Nếu dự án còn dùng jQuery, cấu hình một lần bằng $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': … } }).
Axios và SPA: cookie XSRF-TOKEN
Mỗi response, Laravel gửi kèm cookie mã hóa XSRF-TOKEN. Một số thư viện như Axios và Angular tự đọc cookie này và gửi lại qua header X-XSRF-TOKEN với request cùng origin, nên bạn không phải tự gắn token. Nếu bạn làm SPA tách riêng và dùng Laravel làm API, hãy đọc tài liệu Laravel Sanctum: Sanctum lo cả xác thực lẫn CSRF cho trường hợp này. Phần xác thực được nói kỹ hơn trong bài authentication và authorization trong Laravel.
Loại trừ route khỏi kiểm tra CSRF
Webhook từ cổng thanh toán hay dịch vụ bên ngoài không thể biết token của bạn, nên route nhận webhook cần được loại trừ. Cách tốt nhất là đặt các route này ngoài nhóm web (ví dụ trong routes/api.php). Nếu vẫn cần để trong web.php, khai báo trong bootstrap/app.php:
// bootstrap/app.php — Laravel 13
->withMiddleware(function (Middleware $middleware): void {
$middleware->preventRequestForgery(except: [
'webhook/thanh-toan',
'stripe/*',
]);
})
// bootstrap/app.php — Laravel 11, 12
->withMiddleware(function (Middleware $middleware): void {
$middleware->validateCsrfTokens(except: [
'webhook/thanh-toan',
]);
})
Route đã loại trừ phải tự xác minh nguồn gửi, thường bằng chữ ký (signature) mà nhà cung cấp webhook gửi kèm. Đừng loại trừ cả nhóm route chỉ để "hết lỗi 419". Thứ tự và cách đăng ký middleware được giải thích trong bài middleware trong Laravel.
Laravel 13 còn có hai tùy chọn mới cho cùng phương thức này: originOnly: true chỉ dựa vào kiểm tra nguồn gốc (request thất bại nhận mã 403 thay vì 419), và allowSameSite: true cho phép request từ subdomain cùng site, ví dụ dashboard.example.com nhận request từ example.com.
Lỗi 419 Page Expired: nguyên nhân và cách sửa
Khi token thiếu hoặc không khớp, Laravel trả mã 419 với trang "Page Expired". Các nguyên nhân hay gặp:
- Quên
@csrftrong form, hoặc form được dựng bằng JavaScript mà không gắn token. - Session hết hạn: người dùng để trang mở lâu hơn
SESSION_LIFETIME(mặc định 120 phút trongconfig/session.php) rồi mới gửi form. - Cookie session không được lưu: sai
SESSION_DOMAIN, đặtSESSION_SECURE_COOKIE=truenhưng chạy HTTP, hoặc thư mục lưu session không ghi được. - Trang có form bị cache toàn trang (page cache, CDN): mọi người nhận cùng một token cũ không khớp session của họ. Cần loại trang có form khỏi cache hoặc nạp token bằng AJAX.
- AJAX không gửi header
X-CSRF-TOKENhoặc gửi sang domain khác.
Để trải nghiệm tốt hơn khi session hết hạn, có thể bắt lỗi và đưa người dùng về trang trước kèm thông báo thay vì hiện trang 419 trống trơn. Cách giữ lại dữ liệu đã nhập bằng withInput() được trình bày trong bài Laravel validation.
CSRF token có đủ để bảo vệ ứng dụng?
Token chống CSRF chỉ giải quyết một loại tấn công. Vài nguyên tắc nên đi kèm:
- Không đổi dữ liệu bằng GET: middleware chỉ kiểm tra
POST,PUT,PATCH,DELETE. Một routeGET /posts/5/deletesẽ không được bảo vệ. - Giữ cookie SameSite:
config/session.phpmặc định'same_site' => 'lax', trình duyệt không gửi cookie phiên trong phần lớn request POST từ site khác. Đây là lớp bảo vệ bổ sung, không thay token. - Chống XSS: nếu trang bị chèn script, kẻ tấn công đọc được token ngay trên trang. In dữ liệu bằng
{{ }}(có escape) thay vì{!! !!}. - Phân quyền ở server: request hợp lệ về CSRF vẫn phải qua kiểm tra quyền bằng Gate hoặc Policy.
Kinh nghiệm từ Hozitech
Hozitech dùng Laravel để phát triển website và hệ thống web theo yêu cầu. Những lỗi CSRF gặp nhiều nhất khi tiếp nhận dự án cũ thường không nằm ở framework mà ở cách dùng: loại trừ quá rộng trong danh sách except, route xóa dữ liệu bằng GET, hoặc bật cache toàn trang cho cả trang có form khiến khách gửi liên hệ bị lỗi 419. Khi rà soát, chúng tôi kiểm tra ba chỗ: danh sách route loại trừ, các route GET có ghi dữ liệu, và cấu hình cache với các trang có form.
Nếu website Laravel của doanh nghiệp bạn đang chạy phiên bản cũ đã hết hỗ trợ bảo mật và cần người rà soát, cập nhật định kỳ, có thể tham khảo dịch vụ chăm sóc website của Hozitech.
Về tác giả
Founder
Xin chào mình là Hiền hiện đang là Founder của Hozitech. Mình là kỹ sư phần mềm có hơn 9+ năm kinh nghiệm trong lĩnh vực thiết kế website, ứng dụng di động... Gần đây mình đang nghiên cứu thêm về lĩnh vực marketing. Mình rất vui khi được chia sẽ những kiến thức và kinh nghiệm của mình đến mọi người!
Câu hỏi thường gặp