Hệ thống thử thách bot (bot-challenge) của Cloudflare có thể âm thầm làm gián đoạn các lượt gửi biểu mẫu HTML thông thường, biến một cú nhấp chuột thanh toán đơn giản thành một ngõ cụt cho người dùng thực. Việc chuyển đổi yêu cầu từ một lệnh POST điều hướng (navigation POST) sang luồng ưu tiên fetch (fetch-first flow) sẽ khôi phục trải nghiệm mà không làm ảnh hưởng đến tính bảo mật.
Tại sao vấn đề này lại quan trọng
Một nhà phát triển đã phát hành một biểu mẫu thanh toán hoạt động hoàn hảo trong mọi bộ kiểm thử (test suite), với curl và trên máy chủ cục bộ. Tuy nhiên, chính biểu mẫu đó, khi khách hàng sử dụng Chrome, lại ném ra lỗi bảo mật sau cú nhấp chuột đầu tiên và thông báo “timeout-or-duplicate” ở cú nhấp thứ hai. Sự cố này đã buộc phải thực hiện ba bản phát hành sửa lỗi khẩn cấp (hot-fix) và mất cả một ngày để gỡ lỗi (debugging).
Lớp biên ẩn giấu
Biểu mẫu này nằm trong một gói Astro mã nguồn mở dựa trên phần tử <form> HTML thuần túy. Khi người dùng nhấp vào Pay, máy chủ phản hồi bằng một lệnh chuyển hướng 303 đến Stripe, và trình duyệt sẽ thực hiện chuyển hướng đó mà không cần bất kỳ JavaScript nào. Các trang web thường sử dụng mô hình này như một phương án dự phòng khi các tập lệnh bị vô hiệu hóa.
Cloudflare nằm phía trước trang web và chạy một công cụ phát hiện bot. Đối với các yêu cầu GET thông thường, nó có thể hiển thị một thử thách trung gian (như CAPTCHA hoặc kiểm tra JavaScript). Sau khi trình duyệt vượt qua thử thách, yêu cầu sẽ được tiếp tục.
Tuy nhiên, một lệnh POST điều hướng (navigation POST) không thể bị tạm dừng để thực hiện thử thách rồi sau đó tiếp tục với phần thân (body) còn nguyên vẹn. Lớp biên (edge) sẽ hủy bỏ yêu cầu và trả về trạng thái 503, khiến trình duyệt hiển thị một trang trắng hoặc một lỗi chung chung. Các trình duyệt kiểm thử tự động, vốn mang cùng một dấu vân tay (fingerprint) mà Cloudflare tin tưởng, sẽ không bao giờ kích hoạt thử thách, vì vậy vấn đề vẫn không lộ diện cho đến khi một người dùng thực sự truy cập trang web.
Những gì nhật ký (logs) đã tiết lộ
Một bản truy vết mạng (network trace) trực tiếp từ phiên Chrome của người dùng cho thấy hai yêu cầu trái ngược nhau đến cùng một điểm cuối (endpoint):
- Navigation POST → phản hồi 503, tab bị treo.
- fetch() POST → yêu cầu hoàn tất.
Cả hai yêu cầu đều bắt nguồn từ cùng một nguồn (origin), mang cùng một thông tin xác thực (credentials) và xảy ra tại cùng một thời điểm. Sự khác biệt duy nhất là phương thức truyền tải. Yêu cầu fetch() đã bỏ qua luồng trung gian vốn chặn các lệnh POST điều hướng.
Những hướng đi không lối thoát
Nhà phát triển đã thử một loạt các cách khắc phục nhưng đều không chạm tới nguyên nhân gốc rễ:
- Làm mới các mã thông báo (token) Turnstile, vì cho rằng chúng đã hết hạn.
- Đưa các dải IP vào danh sách trắng (whitelist), vì nghĩ rằng việc chặn dựa trên vị trí.
- Vô hiệu hóa các tiện ích mở rộng, xóa service workers và xóa cookie.
Mỗi thay đổi đều không giải quyết được lỗi vì sự cố bắt nguồn từ phía trên (upstream) tại lớp biên, chứ không phải ở mã nguồn phía client hay server.
Cách khắc phục thực tế
Thay vì tắt tính năng bảo vệ của Cloudflare, biểu mẫu đã được thiết kế lại để sử dụng mô hình "fetch-first":
- Thu thập dữ liệu biểu mẫu và gửi nó bằng
fetch()dưới dạng một JSON payload. - Xử lý phản hồi của máy chủ. Nếu máy chủ trả về một URL cho cổng thanh toán, hãy gọi
location.assign()để điều hướng đến đó bằng một yêu cầu GET đơn giản.
Các yêu cầu fetch() không kích hoạt thử thách trung gian, vì vậy lệnh POST sẽ đến được máy chủ gốc (origin server). Lệnh chuyển hướng GET tiếp theo có thể an toàn đi qua bất kỳ thử thách nào, vì thân (body) của yêu cầu GET là trống và có thể được thực hiện lại sau khi người dùng vượt qua thử thách.
Rủi ro đối với các nhà phát triển
- Niềm tin của người dùng: Một biểu mẫu thanh toán âm thầm thất bại sẽ làm xói mòn sự tin tưởng và có thể dẫn đến mất doanh thu.
- Chi phí bảo trì: Sự cố này đòi hỏi ba bản phát hành vá lỗi và một ngày điều tra toàn diện.
- Điểm mù trong kiểm thử: Việc chỉ dựa vào môi trường kiểm thử nội bộ có thể bỏ lỡ các lỗi ở các trường hợp biên (edge-case) vốn chỉ xuất hiện trong thực tế.
Bài học cho cộng đồng
- Sử dụng trình duyệt thực tế để đo lường. Khi một vấn đề chỉ xuất hiện với người dùng thực, hãy thu thập nhật ký mạng (network logs) từ các phiên đó thay vì chỉ tin tưởng vào các lượt chạy kiểm thử tự động.
- Coi lớp biên (edge) là một phần của hệ thống. Cloudflare nằm giữa client và server; hành vi của nó ảnh hưởng đến cách cấu trúc các yêu cầu.
- Chọn phương thức truyền tải phù hợp. Các lệnh POST điều hướng và POST qua fetch di chuyển qua các lộ trình khác nhau tại lớp biên. Hãy thiết kế API có tính đến sự khác biệt này.
- Hiển thị chi tiết lỗi. Đưa các thông báo lỗi 503 hoặc “timeout-or-duplicate” lên giao diện người dùng (UI) để các nhà phát triển có thể thấy chính xác chế độ lỗi mà không cần phải lục tìm trong nhật ký.
Những điều cần lưu ý tiếp theo
Các nhà phát triển nên kiểm tra lại bất kỳ quy trình làm việc dựa trên biểu mẫu nào dựa trên điều hướng POST thuần túy, đặc biệt là khi Cloudflare hoặc các dịch vụ bảo mật CDN tương tự nằm phía trước trang web. Việc thêm một trình bao bọc (wrapper) fetch nhẹ nhàng có thể ngăn chặn các lỗi tương tự. Các công cụ giám sát có khả năng thu thập các mã trạng thái do lớp biên tạo ra sẽ cảnh báo vấn đề trước khi nó đến tay khách hàng.
Bài học rút ra: Khi các thử thách bot của Cloudflare đang hoạt động, việc gửi biểu mẫu HTML thông thường rất dễ gặp phải lỗi im lặng (silent failure). Việc điều hướng yêu cầu POST thông qua fetch() và hoàn tất quy trình bằng một lệnh chuyển hướng GET sẽ giúp vượt qua hạn chế của edge mà vẫn đảm bảo tính bảo mật. Hãy coi edge là mã nguồn, chứ không chỉ đơn thuần là một bước nhảy mạng, và thiết kế các phương thức truyền tải của bạn một cách phù hợp.
