White-Box Auditing & Săn Bug Thực Chiến
~13 phútRace Conditions & Concurrency ExploitsMiễn phí

Bản chất Concurrency: Cửa sổ Race Window và Limit Overrun

Giải mã nguyên lý Time-of-Check to Time-of-Use (TOCTOU): Khai thác rút tiền vượt số dư, áp dụng mã giảm giá nhiều lần và ghi đè trạng thái phiên.

TL;DR: Hầu hết các ứng dụng web chạy trên mô hình đa tiến trình hoặc đa luồng (Multi-threading). Khi lập trình viên viết logic kiểm tra số dư trước khi trừ tiền: if (balance >= amount) { balance -= amount; }, một khoảng cách thời gian nhỏ (vài mili-giây) xuất hiện giữa bước Check và bước Act. Nếu kẻ tấn công gửi đồng thời hai request rút tiền chạm tới máy chủ trong cùng khoảng thời gian này, cả hai luồng đều đọc được số dư cũ trước khi số dư mới được cập nhật, dẫn tới việc rút thành công gấp đôi số tiền đang có (Limit Overrun).

1. Scenario — Một chiếc vé duy nhất bán cho hai người

Một hệ thống phân phối vé hòa nhạc trực tuyến mở bán đợt vé VIP giới hạn số lượng. Mỗi tài khoản chỉ được phép mua đúng 1 vé duy nhất. Đoạn mã xử lý đặt vé ở tầng backend được viết như sau:

Python
# Vulnerable Checkout Handler
@app.route('/api/v1/tickets/purchase', methods=['POST'])
def purchase_ticket():
    user_id = g.user_id
    ticket_id = request.json.get('ticket_id')

    # 1. CHECK: Verify user purchase count and ticket availability
    already_bought = db.query("SELECT COUNT(*) FROM orders WHERE user_id = %s", (user_id,)).scalar()
    if already_bought > 0:
        return jsonify({"error": "Moi tai khoan chi duoc mua 1 ve"}), 400

    ticket = db.query("SELECT is_sold FROM tickets WHERE id = %s", (ticket_id,)).first()
    if ticket.is_sold:
        return jsonify({"error": "Ve da het"}), 400

    # 2. ACT: Process payment and record order
    time.sleep(0.05) # Simulate external payment gateway latency
    db.execute("INSERT INTO orders (user_id, ticket_id) VALUES (%s, %s)", (user_id, ticket_id))
    db.execute("UPDATE tickets SET is_sold = true WHERE id = %s", (ticket_id,))
    
    return jsonify({"status": "Thanh cong", "ticket_id": ticket_id}), 200

Một người dùng gửi đồng thời 2 HTTP request mua vé gần như cùng một tích tắc. Luồng 1 thực hiện câu lệnh SELECT và thấy already_bought == 0. Trước khi Luồng 1 kịp chạy lệnh INSERT, Luồng 2 chen ngang vào và cũng thực hiện câu lệnh SELECT, kết quả vẫn là already_bought == 0.

Hậu quả là cả 2 luồng đều vượt qua khâu kiểm tra và cùng chạy lệnh INSERT. Người dùng này mua thành công 2 chiếc vé VIP, vi phạm trực tiếp quy tắc nghiệp vụ của hệ thống.

2. Analogy — Hai người cùng rút tiền tại hai cây ATM

Hãy hình dung một tài khoản ngân hàng chỉ có đúng 100.000 VNĐ:

  • Người chồng cầm thẻ chính đứng ở cây ATM Quận 1.
  • Người vợ cầm thẻ phụ đứng ở cây ATM Quận 3.
  • Cả hai cùng bấm nút "Rút 100.000 VNĐ" vào đúng giây thứ 00.

Nếu hệ thống ngân hàng xử lý theo kiểu: Cây ATM 1 đọc số dư thấy 100.000 VNĐ và quyết định nhả tiền; trong khi máy chủ trung tâm chưa kịp trừ tiền trong sổ cái thì Cây ATM 2 cũng đọc số dư và thấy 100.000 VNĐ, nó cũng nhả tiền. Kết quả là 200.000 VNĐ tiền mặt đã được rút ra từ tài khoản chỉ có 100.000 VNĐ.

Khoảng thời gian vài phần trăm giây giữa lúc hệ thống kiểm tra số dư và lúc trừ tiền trong sổ cái chính là Cửa sổ chạy đua (Race Window).

3. Bản chất của Cửa sổ Race Window hoạt động ra sao?

Mô hình Cửa sổ Chạy đua Luồng (Race Window) trong Thao tác Check-then-Act

Nguyên nhân cốt lõi của lỗ hổng Race Condition trong ứng dụng web bắt nguồn từ mô hình kiến trúc:

  1. Ứng dụng phi trạng thái (Stateless Application Servers): Các worker (Gunicorn, Puma, Tomcat, Go goroutines) xử lý request song song trên nhiều CPU cores hoặc nhiều máy chủ khác nhau.
  2. Thao tác Check-then-Act bị chia cắt:
    • T1: Luồng A đọc trạng thái từ Database vào RAM của Worker A (Time of Check - TOC).
    • T2: Luồng B đọc trạng thái từ Database vào RAM của Worker B.
    • T3: Luồng A cập nhật Database (Time of Use - TOU).
    • T4: Luồng B cập nhật Database dựa trên dữ liệu cũ ở bước T2!

Toàn bộ khoảng thời gian giữa T1 và T3 chính là Race Window. Nếu trong khoảng thời gian này có bất kỳ thao tác I/O nặng (như gọi API ngân hàng, gửi email, phân tích file), Race Window sẽ bị kéo dài từ vài mili-giây lên tới hàng giây, khiến việc khai thác trở nên cực kỳ dễ dàng.

4. Các dạng biến thể Limit Overrun phổ biến trong thực tế

Kỹ thuật chạy đua luồng thường được áp dụng vào 4 nhóm nghiệp vụ nhạy cảm:

4.1. Mã giảm giá dùng 1 lần (Single-Use Coupon Stacking)

Ứng dụng cho phép mỗi khách hàng dùng mã giảm 50% đúng một lần. Bằng cách gửi 10 request áp dụng mã cùng lúc vào giỏ hàng, người dùng có thể áp dụng mã 5–10 lần, kéo giá trị đơn hàng về 0 đồng.

4.2. Khai thác tính năng nạp thẻ quà tặng (Gift Card Redemption)

Sau khi kiểm tra mã thẻ hợp lệ, hệ thống cộng tiền vào ví rồi mới đánh dấu thẻ đã sử dụng (is_redeemed = true). Gửi 5 request đồng thời giúp nạp gấp 5 lần mệnh giá thẻ quà tặng.

4.3. Đăng ký vượt hạn ngạch (Quota & Rate Limit Bypass)

Tài khoản miễn phí chỉ được tạo tối đa 3 dự án (projects). Gửi đồng thời 20 request tạo dự án giúp kẻ tấn công sở hữu 20 dự án mà không cần nâng cấp lên gói trả phí.

Dự đoán hành vi: Nguyên nhân gây mở rộng Race Window

Hành động nào của lập trình viên bên trong khối Check-then-Act sẽ làm tăng nguy cơ bị khai thác Race Condition lên cao nhất?

5. Pitfalls & Giải pháp phòng thủ triệt để

❌ Bẫy thường gặp: Khóa bằng Application Mutex trong bộ nhớ

Nhiều lập trình viên dùng synchronized trong Java hoặc sync.Mutex trong Go để khóa hàm xử lý. Cách này chỉ có tác dụng nếu ứng dụng chạy trên đúng 1 tiến trình và 1 container duy nhất. Khi triển khai trên Kubernetes với 3 pods hoặc nhiều server, mỗi pod có một vùng nhớ riêng; Mutex của pod A hoàn toàn không thể ngăn chặn luồng đang chạy trên pod B!

✅ Giải pháp chuẩn mực: Atomic Updates và Database Constraints

  1. Ràng buộc duy nhất ở tầng cơ sở dữ liệu (Unique Constraint): Trong bài toán vé hòa nhạc, đặt ràng buộc duy nhất: ALTER TABLE orders ADD CONSTRAINT unique_user_ticket UNIQUE (user_id);. Bất kể có bao nhiêu luồng chạy song song, cơ sở dữ liệu sẽ chỉ cho phép duy nhất 1 lệnh INSERT thành công, các lệnh còn lại bị CSDL ném lỗi duplicate key ngay lập tức.
  2. Cập nhật nguyên tử trong một câu lệnh duy nhất (Atomic Single Update): Thay vì SELECT rồi UPDATE, gộp chung điều kiện vào câu lệnh UPDATE:
    SQL
    UPDATE accounts 
    SET balance = balance - 100 
    WHERE user_id = 1 AND balance >= 100;
    
    Kiểm tra số dòng bị ảnh hưởng (rows_affected). Nếu bằng 1 là thành công, nếu bằng 0 là không đủ số dư. Thao tác này được engine của Postgres/MySQL khóa tự động ở cấp độ hàng (Row-level lock), miễn nhiễm 100% trước mọi cuộc tấn công race condition.

6. Liên hệ các bài khác

Hiểu rõ bản chất Race Window là bước đệm để làm chủ kỹ thuật khai thác cấp cao:

7. Tóm tắt

  • Race Condition xảy ra khi logic kiểm tra trước thực thi sau (Check-then-Act) bị chen ngang bởi các luồng xử lý đồng thời.
  • Race Window là khoảng cách thời gian giữa lúc đọc trạng thái cũ và lúc cập nhật trạng thái mới.
  • Lỗ hổng Limit Overrun gây thiệt hại tài chính trực tiếp qua việc rút tiền kép hoặc áp dụng mã giảm giá nhiều lần.
  • Khóa trong bộ nhớ ứng dụng (In-memory locks) hoàn toàn vô tác dụng trong môi trường microservices phân tán.
  • Biện pháp phòng thủ tối thượng là sử dụng Atomic SQL Updates và Unique Database Constraints.

8. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao việc đặt cờ synchronized trong Java hoặc sync.Mutex trong Go lại thất bại trong việc ngăn chặn Race Condition trên môi trường production hiện đại?
  2. Q2
    Trong câu lệnh 'UPDATE accounts SET balance = balance - 50 WHERE id = 1 AND balance >= 50', cơ sở dữ liệu ngăn chặn Race Condition như thế nào?
  3. Q3
    Hiện tượng 'Sub-state race condition' là gì?
  4. Q4
    Tại sao các tác vụ I/O mạng bên ngoài (External Network Calls) đặt bên trong transaction cơ sở dữ liệu lại là một thảm họa thiết kế?
  5. Q5
    Làm thế nào để phát hiện các đoạn code có nguy cơ dính Race Condition khi thực hiện White-box code review?

Bài tiếp theo: Kỹ thuật Single-Packet Attack trên HTTP/2: Triệt tiêu Network Jitter

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Kỹ thuật Single-Packet Attack trên HTTP/2: Triệt tiêu Network Jitter