5 phút đọc

Laravel: gửi số 0 thay cho token là đăng nhập được vào tài khoản người khác

Lỗi CVE-2026-114509 (CVSS 9,2) trong guard token của Laravel cho người lạ đăng nhập API bằng tài khoản có sẵn, thường là admin, nếu app dùng MySQL hoặc MariaDB. Đã có mã khai thác công khai. Nâng lên 12.68.0 hoặc 13.29.0 và kiểm tra config/auth.php.

Xem nhanh trong 1 phút

Token API của bạn dài 60 ký tự ngẫu nhiên, đoán cả đời không ra. Nhưng với một app Laravel chạy MySQL, kẻ tấn công không cần đoán. Họ gửi {"api_token": [0]} và database trả về một user có thật, thường là user tạo đầu tiên, tức tài khoản admin.

Robot linh vật nhìn cánh cửa tự mở khi đưa vào tấm thẻ trắng trơn

Đó là lỗ hổng CVE-2026-114509 trong guard token của Laravel, được chấm 9,2/10 theo CVSS v4 (GitLab chấm 9,8 theo CVSS 3.1). Laravel công bố khuyến cáo ngày 19/10. Ngay sau đó, nhóm Hakai Security đưa lên GitHub một bản PoC kèm môi trường Docker dựng sẵn, và thêm ít nhất một kho PoC khác xuất hiện. Đến sáng 24/10 chưa có nguồn nào xác nhận lỗi bị khai thác thật, nhưng mã khai thác đã nằm sẵn đó cho ai muốn thử.

Lỗi nằm ở đâu

Guard token là cách xác thực API đơn giản nhất của Laravel: bảng users có cột api_token, client gửi token lên, Laravel tìm user có token khớp. Rất nhiều bài hướng dẫn cũ dùng cách này, và AI viết code Laravel cũng có lúc chọn nó vì ít bước cài.

Trước bản vá, lớp TokenGuard chỉ kiểm token có rỗng không. Nó không kiểm token có phải là chuỗi hay không. Nếu client gửi một mảng, mảng đó đi thẳng xuống EloquentUserProvider, và hàm này thấy giá trị là mảng thì dựng câu truy vấn WHERE api_token IN (...).

Phần còn lại do MySQL. Khi so một cột chữ với một số, MySQL đổi chữ sang số. Chuỗi bắt đầu bằng chữ cái, như aB3x..., đổi ra thành 0. Vậy WHERE api_token IN (0) khớp với mọi token bắt đầu bằng chữ cái, và Laravel lấy user đầu tiên trong số đó.

Theo mô tả trong kho PoC:

  • Phải gửi bằng body JSON (POST, PUT, PATCH), vì JSON giữ được số nguyên 0. Gửi ?api_token[]=0 trên URL thì 0 thành chuỗi "0", không khớp.
  • Kẻ tấn công không chọn được người, chỉ lấy được user khớp đầu tiên. Với app mới dựng, đó thường là tài khoản admin.
  • PostgreSQL và SQLite không bị, vì hai database này không tự đổi kiểu như vậy.
  • Bật 'hash' => true cho guard thì trên PHP 8 Laravel gọi hash() với một mảng và báo lỗi, nên không lọt. Khuyến cáo chính thức không nhắc tới chi tiết này.

Bản vá chỉ là một dòng: if (is_string($token) && ! empty($token)). Thiếu đúng dòng kiểm kiểu đó, người lạ vào được API bằng tài khoản admin.

Ai bị ảnh hưởng

Bạn cần lo nếu cả ba điều sau đều đúng:

  • laravel/framework thấp hơn 12.68.0 (nhánh 12) hoặc thấp hơn 13.29.0 (nhánh 13)
  • config/auth.php có guard dùng 'driver' => 'token'
  • Bảng user nằm trên MySQL hoặc MariaDB

Sanctum và Passport không bị, vì hai gói này có guard riêng. Phần lớn app Laravel mới dùng Sanctum, nên đừng hoảng nếu bạn chỉ dùng auth:sanctum.

Chuyện lạ của vụ này: hai bản 12.68.0 và 13.29.0 đã phát hành từ 25/8/2026, gần hai tháng trước khi khuyến cáo ra. App nào cập nhật thường xuyên thì đã được vá từ lâu mà không biết. Còn app khóa cứng phiên bản từ đầu năm thì đang hở.

Laravel 11 trở xuống không có trong danh sách bản vá chính thức. Kho PoC có nhắc bản 11.56.1, nhưng khuyến cáo của Laravel chỉ liệt kê 12.68.0 và 13.29.0. Còn chạy Laravel 11 hay cũ hơn thì nên tính chuyện lên nhánh 12.

Kiểm tra app của bạn

Ở thư mục dự án, xem phiên bản đang chạy:

composer show laravel/framework | grep versions
composer audit

composer audit sẽ báo GHSA-f9x3-7vq9-cph6 nếu bạn đang ở bản lỗi. Tiếp theo, tìm guard token:

grep -n "'driver' => 'token'" config/auth.php

Có kết quả và đang ở bản lỗi thì nâng cấp:

composer update laravel/framework
php artisan --version

Rồi deploy lại. Trên máy chủ chạy PHP-FPM hay Octane, khởi động lại tiến trình để code mới được nạp.

Chưa nâng cấp được ngay, bạn có thể chặn tạm bằng một middleware đặt trước guard, từ chối mọi request có api_token không phải chuỗi:

// app/Http/Middleware/ApiTokenPhaiLaChuoi.php
public function handle($request, Closure $next)
{
    if ($request->has('api_token') && ! is_string($request->input('api_token'))) {
        abort(401);
    }
    return $next($request);
}

Đây là cách vá tạm do mình đề xuất, Laravel không đưa ra cách nào ngoài nâng cấp. Nâng cấp xong thì bỏ nó đi.

Nếu app đã mở ra internet một thời gian với cấu hình dính lỗi, hãy xem log truy cập các route API có request nào gửi body JSON chứa "api_token":[ không, và tạo lại token cho các tài khoản quản trị.

Cùng một kiểu lỗi, ở code bạn tự viết

Lỗi này không riêng gì Laravel. Bất cứ chỗ nào lấy thẳng giá trị từ body JSON rồi đưa vào câu truy vấn mà không kiểm kiểu đều có thể bị tương tự. Ví dụ quen thuộc với Express và MongoDB: gửi {"token": {"$ne": null}} thì User.findOne({ token: req.body.token }) trả về user đầu tiên có token.

Cách chặn ở mọi ngôn ngữ đều giống nhau: trước khi tra database, kiểm token là chuỗi, đúng độ dài bạn cấp ra. Với Node.js, một dòng if (typeof token !== 'string') return res.sendStatus(401) là đủ. Dùng thư viện kiểm dữ liệu như Zod, Pydantic hay Form Request của Laravel thì khai báo rõ kiểu string cho trường token.

Khi nhờ AI viết phần đăng nhập API, bạn có thể dặn thêm một câu: “kiểm kiểu dữ liệu của token trước khi truy vấn”. Câu dặn đó rẻ hơn nhiều so với một tài khoản admin bị chiếm.


App Laravel của bạn đang xác thực API bằng gì, token cũ, Sanctum hay tự viết? Vào cộng đồng Vibe Code & Bảo Mật kể cho mọi người nghe nhé.

Nguồn

Chia sẻ lên Facebook

Bình luận

Đang tải bình luận…