Xóa mù lỗ hổng web (Phần 3): Lỗi thiết kế, API và logic nghiệp vụ — nơi công cụ quét bó tay
Hai phần trước nói về những lỗi có thể chỉ tận tay trong một dòng code: một câu truy vấn bị nối chuỗi, một endpoint quên kiểm tra quyền, một tài khoản mặc định còn nguyên trên production. Phần này khó hơn. Ở đây code chạy đúng như đã viết, cấu hình sạch sẽ, công cụ quét không báo gì — nhưng chính ý tưởng ban đầu đã sai. Không công cụ nào nói cho bạn biết rằng cho phép đổi email nhận hóa đơn mà không cần xác thực lại là một thiết kế tồi.
OWASP gọi nhóm này là Insecure Design và nhấn mạnh một điều đáng nhắc lại: lỗi thiết kế không thể vá bằng cách viết code gọn hơn. Khi sản phẩm ngày nay thực chất là một tập hợp API phục vụ web, mobile và đối tác cùng lúc, phần lớn rủi ro đã dịch chuyển khỏi trình duyệt để nằm ở tầng API và các luồng nghiệp vụ phía sau nó.
Bài viết này dành cho lập trình viên đã qua giai đoạn học cú pháp, cùng QA và PM — những người trên thực tế phát hiện lỗi logic nhiều hơn cả đội bảo mật.
Bản đồ thay đổi giữa OWASP Top 10:2021 và OWASP Top 10:2025. Nguồn: OWASP Foundation, giấy phép CC BY-SA 4.0.1. Insecure Design (A06) — lỗ hổng sinh ra từ bản vẽ
Cần phân biệt cho rõ. Lỗi triển khai là "định làm đúng nhưng viết sai". Lỗi thiết kế là "chưa từng nghĩ tới tình huống đó". Vài ví dụ quen thuộc: quy trình đặt lại mật khẩu dựa trên những thông tin ai cũng tra được; chức năng đặt chỗ không giới hạn số lượt giữ nên bot quét sạch hàng; luồng hoàn tiền cho phép người bán tự duyệt giao dịch của chính mình; hoặc một mã giảm giá được phát hành mà không ai kịp hỏi chuyện gì xảy ra nếu một người dùng nó một nghìn lần.
Cách phòng chống
Hãy mô hình hóa mối đe dọa ngay khi còn đang vẽ luồng, chứ không phải tuần trước ngày go-live. Một buổi họp một tiếng với ba câu hỏi mộc mạc — ai muốn phá cái này, họ được gì, họ sẽ thử đường nào — thường hiệu quả hơn nhiều tuần rà code. Bên cạnh mỗi user story, hãy viết thêm một abuse story dạng "kẻ tấn công cố làm X để đạt được Y". Đưa yêu cầu bảo mật thẳng vào tiêu chí nghiệm thu để nó được kiểm thử như mọi yêu cầu khác. Với các luồng dính tới tiền, dữ liệu cá nhân hoặc quyền quản trị, hãy chuẩn hóa thành mẫu thiết kế dùng lại thay vì để mỗi đội tự nghĩ một kiểu. Và mọi tài nguyên đều phải có hạn mức: giới hạn số lần, giới hạn số lượng và giới hạn thời hạn.
2. BOLA — vẫn là IDOR, nhưng ở quy mô API
Trong OWASP API Security Top 10, Broken Object Level Authorization đứng ở vị trí số một, và nó chính là lỗi IDOR của phần hai khoác áo mới. Điểm khác là bán kính thiệt hại: một API nội bộ thường phục vụ cùng lúc website, ứng dụng di động, kiosk và tích hợp của đối tác, nên một endpoint quên kiểm tra chủ sở hữu sẽ mở dữ liệu cho tất cả các kênh đó. GraphQL còn khó hơn, vì một truy vấn có thể đi qua nhiều tầng quan hệ trong khi việc kiểm tra quyền chỉ diễn ra ở lớp ngoài cùng.
Phòng chống: đặt kiểm tra quyền ở nơi gần dữ liệu nhất — trong repository hoặc service, không phải trong controller — và dùng chung một hàm kiểm tra cho mọi kênh; với GraphQL thì kiểm tra trong từng resolver. Định danh khó đoán như UUID là thứ nên dùng, nhưng đó là biện pháp làm khó chứ không phải kiểm soát truy cập, và tuyệt đối không thay thế được việc kiểm tra quyền sở hữu.
3. Nhận thừa và trả thừa
Hai mặt của cùng một thói quen lười. Nhận thừa là mass assignment: bạn map thẳng toàn bộ body request vào model, người dùng gửi kèm trường vai trò hoặc trạng thái đã thanh toán, và hệ thống ngoan ngoãn ghi xuống cơ sở dữ liệu. Trả thừa là hình ảnh phản chiếu: API trả nguyên bản ghi còn giao diện chỉ hiển thị ba trường, trong khi số điện thoại, địa chỉ và ghi chú nội bộ vẫn nằm nguyên trong phản hồi cho bất kỳ ai mở tab Network.
Phòng chống: khai báo tường minh danh sách trường được phép nhận cho từng endpoint thay vì nhận tất; dùng một đối tượng riêng cho dữ liệu đầu vào và một bộ tuần tự hóa riêng cho đầu ra thay vì phơi thẳng model của cơ sở dữ liệu; coi tài liệu OpenAPI là hợp đồng và viết test tự động khẳng định phản hồi không chứa gì ngoài hợp đồng đó; và đừng bao giờ trông chờ frontend che giấu dữ liệu hộ mình.
4. Không giới hạn tài nguyên — lỗ hổng tính bằng tiền
Một endpoint gửi mã xác thực qua SMS mà không giới hạn tần suất là một cái vòi tiền để mở sẵn cho kẻ tấn công. Tương tự là API tìm kiếm sẵn sàng trả về một trăm nghìn bản ghi trong một lần gọi, truy vấn GraphQL lồng nhau chục tầng, chức năng xuất báo cáo chạy đồng bộ, hay tính năng sinh ảnh gọi tới một mô hình AI tính tiền theo từng request.
Phòng chống: đặt giới hạn tần suất theo người dùng, theo IP và theo endpoint, với ngưỡng chặt hơn cho các thao tác tốn kém; ép giới hạn phân trang ở phía server; giới hạn độ sâu và độ phức tạp của truy vấn GraphQL; đặt timeout và ngắt mạch cho mọi lời gọi ra ngoài; đẩy việc nặng sang hàng đợi kèm hạn mức; và theo dõi chi phí như một chỉ số bảo mật, vì hóa đơn tăng đột biến thường là dấu hiệu sớm nhất của việc bị lạm dụng.
5. Lỗi logic nghiệp vụ và cuộc đua vài mili giây
Đây là nhóm mà pentester giỏi kiếm được tiền còn công cụ tự động thì bó tay, đơn giản vì không máy nào biết quy tắc kinh doanh của bạn. Vài dạng kinh điển: giá sản phẩm được gửi lên từ client rồi server tin luôn; nhảy cóc trong quy trình nhiều bước để tới thẳng bước xác nhận mà chưa trả tiền; hoàn tiền hai lần cho cùng một đơn; và race condition — bắn hai mươi request đổi điểm cùng lúc trong khi hệ thống chỉ kiểm tra số dư một lần rồi mới trừ.
Phòng chống: mọi giá trị có ý nghĩa tiền bạc đều phải tính lại ở server; trạng thái quy trình phải được lưu và kiểm tra ở server sau mỗi bước; dùng khóa ở tầng cơ sở dữ liệu hoặc mức cô lập giao dịch phù hợp cho thao tác trừ số dư, kèm một ràng buộc duy nhất làm chốt chặn cuối cùng; hỗ trợ khóa idempotency cho các API tạo giao dịch để việc thử lại không sinh bản ghi thừa. Về phía kiểm thử, hãy chủ động bắn nhiều request song song vào cùng một tài nguyên — rất ít đội chạy bài test này, và gần như lần nào nó cũng cho ra điều thú vị.
6. Khi trang của bạn chạy code của người khác
Một trang thanh toán bình thường còn nạp thêm script quảng cáo, đo lường, widget chat hỗ trợ và thư viện từ CDN công cộng. Mỗi script đó chạy với đúng quyền như code của bạn: đọc được form, đọc được DOM. Đây chính là con đường của những vụ đánh cắp thông tin thẻ kiểu skimming — không ai xâm nhập máy chủ của nạn nhân cả, họ chỉ sửa một tệp bên thứ ba mà tất cả cùng nạp.
Phòng chống: giảm tối đa số script trên các trang nhạy cảm; tự lưu trữ những thư viện quan trọng thay vì trỏ thẳng ra CDN công cộng; dùng Subresource Integrity cho script bên ngoài; khai báo Content-Security-Policy để giới hạn nguồn script được phép chạy; đặt widget bên thứ ba trong iframe có sandbox; và luôn kiểm tra origin khi nhận dữ liệu qua postMessage.
7. Nhiều khách hàng trên cùng một hệ thống
Với sản phẩm SaaS, câu hỏi đắt giá nhất là dữ liệu của khách A có thể lọt sang màn hình của khách B hay không. Rủi ro hiếm khi đến từ một kẻ tấn công kiên trì; nó đến từ một truy vấn báo cáo viết vội quên điều kiện lọc theo tổ chức, hoặc một job chạy nền dùng quyền hệ thống.
Phòng chống: ép việc phân tách khách hàng ở tầng thấp nhất có thể — scope mặc định của ORM hoặc row-level security của cơ sở dữ liệu — thay vì tin rằng mọi lập trình viên sẽ luôn nhớ; lấy định danh tổ chức từ ngữ cảnh phiên, không bao giờ từ tham số request; viết test tự động khẳng định người dùng của tổ chức này không đọc được dữ liệu của tổ chức kia; siết cả trang quản trị nội bộ, vì đó là nơi tập trung nhiều quyền nhất và thường được bảo vệ lỏng nhất; và che dữ liệu cá nhân trong log cũng như trong các luồng phân tích.
Checklist khi chốt thiết kế một tính năng
- Đã có người đặt thành lời câu hỏi ai sẽ lạm dụng tính năng này và bằng cách nào.
- Mọi thao tác đọc và ghi trên một đối tượng đều kiểm tra quyền sở hữu ở phía server.
- Danh sách trường được phép nhận vào và trường được phép trả ra đều khai báo tường minh.
- Mỗi endpoint tốn kém đều có giới hạn tần suất, hạn mức và timeout.
- Giá trị tiền bạc và trạng thái quy trình được tính toán, kiểm tra ở server.
- Thao tác thay đổi số dư được bảo vệ trước việc chạy song song.
- Script bên thứ ba trên trang nhạy cảm đã được rà soát, có SRI và CSP.
- Có test tự động chứng minh dữ liệu không rò rỉ giữa các tổ chức.
Kết
Những lỗ hổng trong phần này không bao giờ xuất hiện trong báo cáo của công cụ quét. Chúng xuất hiện trong biên bản họp thiết kế — hoặc trong sự vắng mặt của buổi họp đó. Tin tốt là đây cũng là nhóm rẻ nhất để sửa, với điều kiện bạn sửa lúc nó còn nằm trên giấy. Phần bốn, cũng là phần cuối của loạt bài, sẽ nói về cách biến toàn bộ những nguyên tắc này thành thói quen vận hành: kiểm thử bảo mật ngay trong CI, quản lý lỗ hổng theo cam kết thời gian, và làm gì trong ngày sự cố thật sự xảy ra.
Nguồn tham khảo
- OWASP Top 10:2025 — owasp.org/Top10/2025
- OWASP API Security Top 10:2023 — owasp.org/API-Security
- OWASP Cheat Sheet Series — cheatsheetseries.owasp.org
- PortSwigger Web Security Academy — portswigger.net/web-security