Từ chối dịch vụ (DoS) thường được hình dung là một cơn lũ lưu lượng — hàng triệu request làm nghẽn băng thông. Nhưng có một loại nguy hiểm và tinh vi hơn: DoS ở tầng ứng dụng, lợi dụng chi phí bất đối xứng. Kẻ tấn công gửi một request nhỏ, nhưng nó khiến máy chủ tiêu tốn rất nhiều — thời gian CPU, bộ nhớ, thời gian. Không cần lưu lượng lớn; chỉ cần tìm đúng chỗ một byte đầu vào tạo ra một giây xử lý. Bài này đo bốn kiểu bất đối xứng đó.

Toàn bộ chạy trong container tự dựng, tự dọn. Không nhắm vào hệ thống của ai.

Bon kieu DoS tang ung dung: chi phi bat doi xung input nho - server lon

Số đo

Ba lần chạy, kết quả nhất quán.

1. ReDoS — regex bùng nổ mũ:

Độ dài input Thời gian máy chủ bận
20 ký tự 0,03 giây
24 ký tự 0,4 giây
28 ký tự 5 giây
30 ký tự ~20 giây

2. Giải nén — tỷ lệ nén khổng lồ:

Giá trị
Kích thước gửi lên (nén) 49 KB
Giải ra 50 MB (tỷ lệ 1.028 lần)

3. Lạm dụng băm mật khẩu chậm:

Giá trị
Một lần băm bcrypt cost 12 0,17 giây
100 request đăng nhập/giây 16,9 giây CPU mỗi giây

4. Tham số điều khiển khối lượng:

Tham số Thời gian
?limit=10.000 ~0 giây
?limit=10.000.000 0,33 giây

Điều đáng nhớ

ReDoS là bất đối xứng cực đoan nhất — 30 ký tự đổi lấy 20 giây. Regex (a+)+$ trông vô hại, nhưng với một chuỗi aaa...aX không khớp, bộ máy regex thử mọi cách chia nhóm các chữ a trước khi kết luận thất bại — số cách tăng theo hàm mũ với độ dài. Bảng cho thấy rõ: mỗi 4 ký tự thêm vào nhân thời gian lên hơn 10 lần. Một request 30 byte giữ một lõi CPU bận 20 giây; chỉ vài request như vậy là làm quá tải cả máy chủ. Đây là lý do ReDoS đặc biệt nguy hiểm: chi phí gửi gần bằng không, chi phí xử lý gần như vô hạn.

Giải nén biến vài KB thành hàng chục MB — và tỷ lệ có thể lớn hơn nhiều. 49 KB nở thành 50 MB, tỷ lệ 1.028 lần. Đây mới là zip bomb "hiền"; một zip bomb thật (nén lồng nhiều lớp) đạt tỷ lệ hàng triệu. Nếu máy chủ giải nén dữ liệu người dùng (upload nén, request gzip, tệp Office) mà không giới hạn kích thước sau khi giải, một tệp nhỏ có thể ngốn hết RAM. Điểm tinh vi: giới hạn kích thước upload không đủ — phải giới hạn kích thước sau khi giải nén.

Chính cơ chế bảo vệ có thể thành vũ khí. Băm mật khẩu chậm (bài băm mật khẩu) cố tình tốn 0,17 giây mỗi lần để chống bruteforce. Nhưng nếu route đăng nhập không giới hạn tần suất, kẻ tấn công gửi 100 request/giây và buộc máy chủ tiêu 16,9 giây CPU mỗi giây thực — nhiều hơn số lõi của phần lớn máy chủ. Cơ chế làm chậm kẻ dò mật khẩu giờ làm chậm chính máy chủ. Đây là lý do bài giới hạn tần suất và bài băm mật khẩu phải đi cùng nhau: băm chậm mà không giới hạn tần suất là tự mở một cửa DoS.

Đường cong ReDoS: đọc con số cho thấy điều gì

Bảng ReDoS đáng nhìn kỹ vì hình dạng của nó là bằng chứng của lỗ hổng. Bốn ký tự thêm vào — từ 20 lên 24, 24 lên 28 — mỗi lần nhân thời gian lên hơn mười lần (0,03 → 0,4 → 5 → 20 giây). Đó là dấu hiệu của tăng trưởng theo hàm mũ: mỗi ký tự thêm khoảng gấp đôi số đường backtracking. Ngoại suy: một chuỗi 40 ký tự sẽ mất nhiều giờ, 50 ký tự mất nhiều ngày. Kẻ tấn công không cần chuỗi dài — 35 ký tự đã đủ để giữ một lõi bận đến mức xem như treo.

Điều này khiến ReDoS khó phát hiện khi test: một chuỗi 20 ký tự chạy trong 0,03 giây, hoàn toàn bình thường, và mọi test chức năng qua. Lỗ chỉ hiện ở đúng vùng độ dài mà không ai thử — và đó là vùng kẻ tấn công nhắm tới. Giống các bài đo khác trong sê-ri, tín hiệu chỉ xuất hiện khi bạn đẩy input tới đúng quy mô, và quy mô đó thường nằm ngoài phạm vi test thông thường.

Vì sao

Bốn kiểu khác nhau nhưng chung một cấu trúc: chi phí xử lý của máy chủ tăng nhanh hơn nhiều so với chi phí gửi của kẻ tấn công. Vẽ tỷ lệ "byte gửi : công máy chủ":

  • ReDoS: tuyến tính gửi → mũ xử lý. Bất đối xứng khủng khiếp nhất.
  • Giải nén: 1 byte gửi → hàng nghìn byte bộ nhớ.
  • Băm chậm: 1 request → 0,17 giây CPU cố định (cao).
  • Tham số khối lượng: 1 con số → khối lượng tuyến tính theo con số đó.

Đây là điều làm DoS tầng ứng dụng khác DoS băng thông: nó không cần nhiều request, chỉ cần request đắt. Một kẻ tấn công một máy, một kết nối, có thể hạ một máy chủ nếu tìm được đúng điểm bất đối xứng. Phòng thủ băng thông (chặn IP gửi nhiều) không bắt được nó, vì lưu lượng nhìn bình thường.

Nghĩa là gì trong thực tế

  • Đặt giới hạn ở mọi trục: thời gian, bộ nhớ, kích thước, khối lượng. Timeout cho mọi thao tác; giới hạn kích thước sau giải nén; trần cho mọi tham số điều khiển khối lượng (limit, page_size, số vòng lặp).
  • Tránh regex có backtracking mũ, hoặc dùng một bộ máy regex tuyến tính (RE2, re2 của Google) cho input người dùng. Kiểm các regex nhận đầu vào tuỳ ý — đặc biệt các mẫu có nhóm lồng lượng từ ((a+)+, (a|a)*).
  • Giới hạn tần suất các endpoint đắt — đăng nhập (băm chậm), tìm kiếm, xuất dữ liệu, bất cứ gì tốn nhiều CPU mỗi request. Nối với bài giới hạn tần suất: đó là lớp chặn DoS tầng ứng dụng.
  • Nghĩ theo chi phí bất đối xứng khi thiết kế. Với mỗi endpoint, hỏi: một request tệ nhất tốn bao nhiêu của máy chủ? Nếu câu trả lời tăng theo một giá trị người dùng điều khiển, đặt trần cho giá trị đó.

Chỗ tôi không kết luận được

Tôi đo chi phí một request bất đối xứng. Để thật sự từ chối dịch vụ, kẻ tấn công cần gửi đủ nhiều request đắt để cạn tài nguyên — con số đó phụ thuộc số lõi, RAM, và cấu hình máy chủ, mà tôi không đo. Tôi chứng minh tính bất đối xứng (input nhỏ, chi phí lớn), là điều kiện cần của DoS tầng ứng dụng; quy mô cần để thật sự sập tuỳ hệ thống.

Và bốn kiểu của tôi là các đại diện phổ biến, không đầy đủ. Còn "billion laughs" (bài XXE), tấn công băm bảng (nhiều key trùng hash bucket), tấn công đọc chậm (Slowloris giữ kết nối mở) — mỗi cái là một biến thể bất đối xứng khác mà tôi không đo.

Thử ba mươi giây

Nhìn các endpoint của bạn và hỏi một câu cho mỗi cái: một request tệ nhất có thể tốn bao nhiêu?

Cụ thể: có regex nào chạy trên input người dùng không (kiểm ReDoS)? Có giải nén dữ liệu người dùng không (kiểm giới hạn sau-giải-nén)? Có endpoint tốn nhiều CPU mà không giới hạn tần suất không (đăng nhập, tìm kiếm)? Có tham số nào điều khiển số lượng công việc mà không có trần không (limit, size)? Mỗi câu "có mà không giới hạn" là một điểm DoS tầng ứng dụng chờ sẵn.