Bài 22 — Session, Cookie và CSRF: các form tạo project và logout của TaskFlow đã dùng @csrf. Bài này kiểm chứng cơ chế bảo vệ thực sự, phân biệt cookie phiên với token và tránh hiểu nhầm rằng mọi POST thiếu token đều phải bị Laravel 13 từ chối.
1. Cookie không phải toàn bộ session
Với session driver database mà TaskFlow cấu hình mặc định, trình duyệt giữ cookie nhận diện phiên; dữ liệu phiên nằm ở backend. Khi chạy nhiều instance, các instance cần truy cập cùng nơi lưu session và dùng cấu hình mã hóa phù hợp. Session array trong test không phải cấu hình để duy trì đăng nhập qua nhiều request thật.
Đăng nhập ở bài 20 đổi session ID; logout invalidate dữ liệu phiên rồi đổi CSRF token. Không lấy user_id do client gửi để thay danh tính trong session. Không in session cookie hay token thật vào log hỗ trợ, ảnh chụp màn hình hoặc tài liệu công khai.
2. Ba cờ cookie giải quyết ba rủi ro
- Secure yêu cầu cookie được gửi qua kết nối bảo mật theo hành vi trình duyệt; bật cho deployment HTTPS.
- HttpOnly hạn chế JavaScript đọc cookie phiên, nhưng không ngăn một script XSS gửi request dưới danh tính người dùng.
- SameSite kiểm soát việc gửi cookie trong ngữ cảnh cross-site; Lax là lựa chọn của bài thực hành, không phải thay thế kiểm tra CSRF.
Domain để null tạo cookie host-only trong response đã kiểm thử. Không mở rộng cookie cho mọi subdomain nếu không có nhu cầu. Cookie XSRF-TOKEN khác cookie session: nó cố ý cho JavaScript đọc để client có thể gửi token theo header; vì vậy không áp kết luận “mọi cookie đều phải HttpOnly” một cách máy móc.
Cấu hình minh họa cho môi trường HTTPS, không áp nguyên vào server phát triển HTTP:
SESSION_DRIVER=database
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax
Kiểm tra tên biến được config/session.php của dự án đọc, migration sessions và cấu hình reverse proxy. Sau khi đổi env trong deployment dùng config cache, xây lại cache theo quy trình release. Test response headers không thay thế kiểm tra cookie thực tế trong trình duyệt đi qua proxy.
3. CSRF trong Laravel 13 có nhánh origin và token
Middleware PreventRequestForgery chấp nhận metadata Sec-Fetch-Site=same-origin. Khi không qua nhánh đó, cấu hình mặc định kiểm tra token trong session. TaskFlow giữ mặc định này, không bật origin-only, không mở allowSameSite và không thêm ngoại lệ cho /projects hay /logout.
Vì vậy test thiếu token phải ghi rõ metadata request. POST không có metadata và thiếu/sai token bị 419; POST same-origin có thể hợp lệ dù không có token. Điều này không có nghĩa nên xóa @csrf khỏi form: token vẫn cần cho nhánh fallback. Không hướng dẫn client giả mạo Sec-Fetch-Site để “sửa” lỗi 419.
CSRF bảo vệ ngữ cảnh trình duyệt có sẵn cookie xác thực, không thay authentication hoặc policy. Một client ngoài trình duyệt tự đặt header nhưng không có phiên của nạn nhân không tự trở thành nạn nhân. XSS trên chính origin lại là rủi ro khác, cần escape output và kiểm soát script.
4. Test phải bật lại kiểm tra thật
Laravel thường bỏ CSRF trong HTTP test. File tests/Feature/SessionCsrfTest.php bind subclass chỉ trong test để runningUnitTests trả false. Không sửa vendor, không đưa subclass này vào bootstrap production. SQLite bộ nhớ cô lập dữ liệu.
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class SessionCsrfTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:',
'database.connections.sqlite.url' => null, 'session.driver' => 'array']);
DB::purge('sqlite');
$this->artisan('migrate', ['--force' => true])->assertSuccessful();
$this->withoutVite();
$this->app->bind(PreventRequestForgery::class, ActiveCsrfForSessionTest::class);
}
public function test_cross_site_write_without_token_is_rejected_before_insert(): void
{
$this->actingAs(User::factory()->create())->withHeader('Sec-Fetch-Site', 'cross-site')
->post('/projects', ['name' => 'Forged'])->assertStatus(419);
$this->assertDatabaseCount('projects', 0);
}
public function test_missing_metadata_uses_token_fallback(): void
{
$this->actingAs(User::factory()->create())->withSession(['_token' => 'session-test-token']);
$this->post('/projects', ['name' => 'Wrong token', '_token' => 'wrong'])->assertStatus(419);
$this->post('/projects', ['name' => 'Valid token', '_token' => 'session-test-token'])
->assertRedirect();
$this->assertDatabaseCount('projects', 1);
}
public function test_laravel_13_accepts_same_origin_metadata_without_token(): void
{
$this->actingAs(User::factory()->create())->withHeader('Sec-Fetch-Site', 'same-origin')
->post('/projects', ['name' => 'Same origin'])->assertRedirect();
$this->assertDatabaseCount('projects', 1);
}
public function test_logout_requires_csrf_when_origin_metadata_is_absent(): void
{
$user = User::factory()->create();
$this->actingAs($user)->withSession(['_token' => 'logout-test-token']);
$this->post('/logout')->assertStatus(419);
$this->assertAuthenticatedAs($user);
$this->post('/logout', ['_token' => 'logout-test-token'])->assertRedirect(route('login'));
$this->assertGuest();
}
public function test_session_cookie_flags_under_https_configuration(): void
{
config(['session.secure' => true, 'session.http_only' => true, 'session.same_site' => 'lax',
'session.domain' => null]);
$response = $this->get('https://localhost/login')->assertOk();
$cookies = collect($response->headers->getCookies())->keyBy(fn ($cookie) => $cookie->getName());
$sessionCookie = $cookies->get(config('session.cookie'));
$this->assertNotNull($sessionCookie);
$this->assertTrue($sessionCookie->isSecure());
$this->assertTrue($sessionCookie->isHttpOnly());
$this->assertSame('lax', $sessionCookie->getSameSite());
$this->assertNull($sessionCookie->getDomain());
$this->assertFalse($cookies->get('XSRF-TOKEN')->isHttpOnly());
}
}
class ActiveCsrfForSessionTest extends PreventRequestForgery
{
protected function runningUnitTests()
{
return false;
}
}php artisan test --filter=SessionCsrfTest
php artisan test
Test khẳng định request cross-site không token bị chặn trước insert; fallback token sai bị chặn và token đúng ghi được; same-origin đi qua; logout thiếu token không làm mất đăng nhập. Test cookie kiểm tra response khi cấu hình HTTPS: cookie session Secure/HttpOnly/Lax/host-only, XSRF-TOKEN không HttpOnly.
Toàn suite pass 66 test, 284 assertions. Test dùng actingAs để cô lập CSRF, không chứng minh toàn bộ vòng đời browser cookie qua mạng. Không có deployment session config nào được thay đổi trong bài này; các cờ được đặt trong test và ví dụ hướng dẫn.
5. Điều tra lỗi 419 theo thứ tự
- Kiểm tra form có _token và token thuộc phiên hiện tại, không phải trang cache của người khác.
- Kiểm tra cookie session có được browser gửi không: host, path, HTTPS và SameSite.
- Kiểm tra backend session dùng chung, thời hạn phiên và cấu hình key giữa các instance.
- Kiểm tra tab cũ sau logout/login hoặc phiên hết hạn; tải lại form thay vì tự gửi lại thao tác ghi không idempotent.
- Đối chiếu cấu hình runtime đã cache và proxy, không chỉ đọc file env.
Không chữa 419 bằng cách exclude toàn bộ web routes, tắt encryption hay log nguyên cookie. Webhook cần xác thực riêng theo chữ ký/provider và phạm vi ngoại lệ hẹp; TaskFlow chưa thêm webhook.
Bài tập: thêm test header X-CSRF-TOKEN thay _token, cookie cấu hình sai Secure trên HTTP local, và tab cũ sau logout. Tham khảo Laravel CSRF và HTTP Session.
Điều hướng: Bài 21 · Lộ trình. Tiếp theo là checklist bảo mật production.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.