Series pragmarx/google2fa Nâng cao #2: Chống Replay Attacks – Vắt kiệt vòng đời 30 giây
1. Bản chất của Replay Attack trong TOTP
Thuật toán TOTP chỉ làm một nhiệm vụ duy nhất: Kiểm tra xem tại thời điểm hiện tại, cái mã 6 số đó có khớp với công thức toán học hay không. Nó KHÔNG HỀ LƯU TRẠNG THÁI (Stateless).
Nghĩa là, trong vòng 30 giây của chu kỳ đó, nếu bạn gọi hàm Google2FA::verifyKey() 100 lần với cùng một mã OTP đúng, nó sẽ trả về true cả 100 lần!
Đây là một tử huyệt. Nguyên tắc tối thượng của One-Time Password (Mật khẩu dùng 1 lần) là: Đã dùng thành công 1 lần thì phải bị vứt bỏ ngay lập tức, dẫu cho nó chưa hết thời gian sống.
2. Thuốc giải từ PragmaRX: Tính năng "Nhớ Timestamp"
Nếu bạn dạo quanh mã nguồn của package trên GitHub, bạn sẽ thấy tác giả Antonioribeiro đã chuẩn bị sẵn một hàm chống Replay Attack tên là verifyKeyNewer().
Hàm này hoạt động bằng cách: Sau khi xác thực thành công, nó sẽ trả về Timestamp (Dấu thời gian) của cái chu kỳ chứa mã OTP đó. Bạn phải lưu cái Timestamp này vào Database (cột google2fa_ts). Ở lần đăng nhập sau, hệ thống sẽ kiểm tra: Mã OTP này có thuộc về một chu kỳ mới hơn cái chu kỳ đã dùng lần trước không?
Tuy nhiên, cách này bắt buộc bạn phải liên tục ghi (Write) vào Database mỗi lần User đăng nhập. Với một hệ thống chịu tải cao như AFC, việc Update Database liên tục chỉ để lưu timestamp là không tối ưu.
3. Giải pháp Enterprise: Blacklist bằng Redis Cache
Thay vì nhớ cái mới, chúng ta sẽ "Phạt thẻ đen" (Blacklist) cái cũ. Kỹ thuật này cực kỳ nhẹ nhàng, không chạm vào Database và rất hoàn hảo cho API phi trạng thái (Stateless).
Logic như sau:
- Nếu mã OTP đúng, ta ghép ID của User và mã OTP đó thành một cái chìa khóa (Cache Key).
- Ném cái chìa khóa đó vào Redis Cache, cho nó sống đúng bằng thời gian $window (ví dụ 1-2 phút).
- Nếu Hacker dùng lại mã đó, ta check Cache thấy chìa khóa tồn tại ➔ Khóa cổ ngay lập tức!
4. Nâng cấp Controller Đăng nhập
Hãy mở lại hàm verify2FA trong AuthController.php (từ series trước) và nâng cấp khiên thép:
use Illuminate\Support\Facades\Cache;
use PragmaRX\Google2FALaravel\Facade as Google2FA;
public function verify2FA(Request $request)
{
// ... (Đoạn lấy thông tin user từ challenge_id) ...
$otpCode = $request->otp_code;
// 1. CHỐT CHẶN REPLAY ATTACK: Kiểm tra trong Blacklist
$blacklistKey = 'used_otp_' . $user->id . '_' . $otpCode;
if (Cache::has($blacklistKey)) {
// Cảnh báo đỏ: Có kẻ đang cố tình dùng lại mã cũ!
// (Trong thực tế, bạn có thể trigger cảnh báo tới kênh Slack của team Security)
return response()->json([
'success' => false,
'message' => 'Mã bảo mật này vừa được sử dụng. Vui lòng chờ mã mới trên ứng dụng.'
], 403);
}
// 2. Thuật toán TOTP kiểm tra tính hợp lệ
$secretKey = decrypt($user->google2fa_secret);
$isValid = Google2FA::verifyKey($secretKey, $otpCode);
if ($isValid) {
// 3. ĐƯA VÀO BLACKLIST NGAY LẬP TỨC
// Tính toán thời gian sống của Cache = (window mặc định + 1) * 30 giây
// Ví dụ window = 1 -> Cache sống 60 giây. Đủ để vắt kiệt vòng đời của mã này.
$windowSeconds = (config('google2fa.window') + 1) * 30;
Cache::put($blacklistKey, true, now()->addSeconds($windowSeconds));
// ... Xóa vé chờ và cấp Access Token ...
return response()->json([
'success' => true,
'access_token' => $token
]);
}
return response()->json(['message' => 'Mã OTP không chính xác.'], 401);
}
Tổng kết Bài 2
Chỉ với vài dòng code thao tác trên Redis (Cache), bạn đã chặn đứng hoàn toàn rủi ro Replay Attack. Hệ thống giờ đây hiểu đúng định nghĩa "One-Time" (Một lần): Bất kể mã còn hiệu lực thời gian hay không, cứ dùng lọt qua cửa là tấm vé đó bị xé rách ngay lập tức. Kẻ đứng sau (Hacker) có lụm được tấm vé đó cũng vô dụng.
Những kỹ năng tối ưu luồng xử lý và kết hợp Redis để giảm tải Database như thế này chính là điều làm nên giá trị của một Senior Backend Developer.
Chúng ta đã xử lý xong phần lỗi thời gian và chống tấn công. Ở Bài 3 (Trạm cuối của Mini-series này), bạn muốn chúng ta khám phá cách tự viết Custom QR Code Renderer để chèn logo công ty vào giữa mã QR (tăng độ chuyên nghiệp cho UI), hay muốn học kỹ thuật Rate Limiting (Giới hạn số lần thử) để chống lại các cuộc tấn công Brute-force (đoán mò 6 số liên tục) vào API xác thực?
All rights reserved