0. Mở đầu
OTP hay One Time Password được sử dụng phổ biến trong nhiều ứng dụng ngày nay như là một lớp xác thực thứ 2 để xác minh người dùng. Concept thì đơn giản thôi: mã OTP gửi tới một 'địa chỉ' đã được xác định trước đó (email, SĐT, etc.), mã OTP này chỉ sử dụng một lần và chỉ có hiệu lực trong một khoảng thời gian ngắn xác định, thường là <=5 phút.
Mặt dù concept đơn giản nhưng xung quanh nó có nhiều khía cạnh bảo mật mà cả Pentester và cả Developer cần nên biết. Từ kinh nghiệm bản thân, nhiều dự án dù được đầu tư kỹ lưỡng về cả mặt quy trình, giấy tờ, con người, thời gian, etc.; nhưng tới bước kiểm thử, đụng tới OTP thì vẫn "lòi" ra một số lỗ hổng đáng kể.
Có thể cái gì mình càng xem nhẹ, thì càng dễ làm sai !
1. Các lỗi cần tránh
Bằng một cách nào đấy chúng vẫn tồn tại!
1.1. Return OTP ngay trong HTTP response
Có thể vì dev muốn tiện lợi hơn trong quá trình phát triển nên triển khai luôn thế này cho nó nhanh (!?). Nhưng tuyệt đối phải tránh khi đưa ứng dụng lên môi trường production.
1.2. OTP chỉ có ý nghĩa ở Front-End
Ứng dụng có vài step trong việc chuyển tiền như sau:
Bước 1: User gửi một request tới back-end để kiểm tra tính "hợp lệ" của người chuyển và người nhận, cũng như một số thông tin liên quan.
Bước 2: Phía sau có một hệ thống rule-based và 'một vài thứ khác nữa' để quyết định xem payment đó có hợp lệ và user có cần phải nhập OTP hay không.
Bước 3: Back-end gửi một message về front-end, front-end dựa vào message này để quyết định bước tiếp theo. Giả định ở đây là user được yêu cầu phải nhập OTP, message sẽ là "REQUIRE_OTP".
Bước 4: Ứng dụng hiển thị một prompt và yêu cầu user nhập OTP vào đó.
Bước 5: Nếu user nhập đúng OTP, ứng dụng sẽ gửi một submit payment request khác tới back-end.
Nghe thì có vẻ cũng hợp lý và đúng flow, nhưng vấn đề ở đây là: Tại bước "4.5", khi người dùng nhập đúng OTP, ứng dụng không trả về bất cứ một secret value/nonce value hay bất cứ thứ gì chứng minh người dùng đã nhập đúng OTP cả. Dẫn tới tại bước 5, người dùng submit payment thoải mái mà không cần thêm giá trị nào ngoài session token như bình thường, ứng dụng không xác định xem actor đang submit payment có đúng là actor có OTP chính xác hay không. Việc hiển thị promp nhập OTP chỉ có ý nghĩa ở Front end. Đây là cái thứ nhất.
Cái thứ 2 là tại Bước 3, người dùng có thể dễ dàng thay đổi HTTP response từ "REQUIRE_OTP" thành một giá trị hợp lệ nào đó, ví dụ "ACCEPT_PAYMENT", và thế là ứng dụng tự động gửi một submit payment request đi. Vậy đấy !
Vấn đề này nằm ở design luôn rồi.
Lỗi này hay xảy ra ở cả các ứng dụng mobile
1.3. Không vô hiệu OTP trước đó hoặc OTP không có expired time cụ thể/quá dài
Dễ hiểu và dễ kiểm tra, hãy đảm bảo với 1 action tại 1 thời điểm chỉ có duy nhất 1 OTP hợp lệ, bất kể user đã yêu cầu gửi lại OTP bao nhiêu lần. Đồng thời đảm bảo expired time của OTP chỉ tính bằng đơn vị x phút (x tiểu học).
1.4. Không có rate limit
1.4.1. Không rate limit khi nhập OTP
Dễ hiểu và dễ kiểm tra, lỗi này dễ dẫn tới một vấn đề là brute-forcing OTP, với OTP 6 chữ số, có 1.000.000 OTP khả dụng, brute-force dần thì cũng hết nhưng Pentester thường sẽ lấy mã OTP đúng, cho chạy tầm vài trăm request để chứng minh lỗi cho nhanh.
Với mã OTP đúng, HTTP response length sẽ khác, mã đúng ở đây là 4383 (OTP 4 chữ số). HTTP response length là 449, khác với 466 khi nhập OTP sai.
1.4.2 Không rate limit khi yêu cầu gửi OTP
Dễ hiểu và dễ kiểm tra, hãy đảm bảo bạn đặt rate limit cho user khi họ yêu cầu OTP mới (và cẩn thận đặt nó ở back-end, đừng chỉ mỗi ở front-end). Thường thời gian phù hợp là 30s - 3 phút tuỳ ứng dụng.
Lỗi 1.4.2 này nếu kết hợp với lỗi 1.3 thì đúng là thảm hoạ
1.5. Có rate limit nhưng lại dẫn tới lỗ hổng User Enumeration
Một số ứng dụng cho phép các bạn login qua một mã số định danh hoặc qua SĐT, họ sẽ gửi SMS OTP về máy của bạn, họ đã implement mọi thứ đẹp đẽ ngon nghẻ, rate limit đàng hoàng, 2 phút bạn mới có thể yêu cầu OTP 1 lần. Nhưng rate-limit lại chỉ áp dụng với...user đã tồn tại trong hệ thống.
Đại khái, trong cả 2 người hợp (SĐT tồn tại và không tồn tại trong hệ thống), app rất thông minh đã respond lại một "general message" như sau: Chúng tôi đã gửi OTP về số điện thoại của bạn, vui lòng abcxyz.
Nhưng khi người dùng bấm "Resend OTP" ngay sau đó, với người dùng không tồn tại, app respond lại "general message" ở trên một lần nữa. Còn với người dùng đã tồn tại, lời nhắn về rate limit hiện lên: "Bạn chỉ có thể yêu cầu OTP mỗi 2 phút ...".
Thế là attacker dễ dàng biết được user nào/SĐT nào đã tồn tại trong hệ thống !
1.6. Không kiểm tra lại nonce value
Back end có trả về nonce value cho client khi nhập đúng OTP, client sử dụng nonce value đó để cho vào request tiếp theo rồi gửi đi. Nhưng bất ngờ chưa phía back end không quan tâm (không kiểm tra) nonce value có đúng hay không và chấp nhận request, tiếp tục xử lý yêu cầu của người dùng.
2. Các cách thức triển khai OTP phổ biến
- Tạo và gán cho user một access token mới sau khi user cung cấp OTP chính xác, cách này thường thấy trong bước đầu của Authentication Scheme (ví dụ đăng nhập vào ứng dụng, đăng nhập qua IdP trong OpenID, etc.).
- Gửi OTP cùng với action cần OTP (ví dụ chuyển tiền).
- Gửi OTP, nếu OTP đúng thì sẽ nhận được một nonce value, gán nonce value đó vào một parameter nào đó ở request sau.
3. Tóm tắt
Một số gợi ý để triển khai OTP:
- Độ dài nên từ 6 ký tự, gửi qua các kênh được user xác nhận từ trước như Email/SĐT
- Expired time của OTP chỉ nên <= 5 phút
- Tại một thời điểm, đối với 1 action chỉ có một OTP hợp lệ (vô hiệu hoá OTP trước đó)
- ÁP dụng rate limite phù hợp ở cả bước nhập, yêu cầu gửi lại OTP và cả response message khi user vi phạm rate limit
Ví dụ: khi user nhập sai OTP quá 5 lần thì lập tức OTP cho action đó bị expired luôn, và response message luôn luôn giống nhau và chung chung theo kiểu: OTP bạn nhập không chính xác, vui lòng abcxyz. Khi đó kể cả có submit đúng OTP thì message vẫn y hệt vì bản chất OTP đó đã expired rồi.
Về response message khi người dùng yêu cầu gửi lại OTP, với cả người dùng đã tồn tại và người dùng không tồn tại trong hệ thống đều nên có response message giống nhau.
- Design ứng dụng sao cho phía back-end server phải chắc chắn biết được rằng actor thực sự có OTP hợp lệ, hầu hết mọi thứ ở front-end để limit action của user là vô nghĩa.
4. Tham khảo
Bài viết này được đúc kết từ kinh nghiệm làm việc sau rất nhiều lần gặp lỗi liên quan tới OTP !