Mở đầu — vì sao bài này quan trọng
Bạn hãy hình dung một buổi sáng thứ Hai bình thường: đội phát triển vừa lên production tính năng "Xem hóa đơn của tôi" cho ứng dụng ví điện tử. Mọi thứ chạy mượt, demo hoàn hảo, khách hàng gật gù. Đến trưa, một người dùng vô tình đổi con số trên thanh địa chỉ từ /invoice/100245 thành /invoice/100246 — và bất ngờ anh ta nhìn thấy hóa đơn của một người hoàn toàn xa lạ, kèm số điện thoại, số tiền giao dịch và bốn số cuối thẻ. Không có ai hack, không có mã độc, không có công cụ tinh vi nào cả. Chỉ là phần mềm quên kiểm tra một câu hỏi đơn giản: "Hóa đơn này có thực sự thuộc về người đang đăng nhập không?"
Đây chính là lý do bài học này tồn tại. Rất nhiều BA, kể cả Technical BA, vẫn nghĩ bảo mật là "việc của đội security" hoặc "việc của dev". Nhưng phần lớn lỗ hổng nghiêm trọng nhất không sinh ra ở tầng hạ tầng — chúng sinh ra ở tầng logic nghiệp vụ, nơi mà chính BA là người viết requirement. Khi bạn viết một user story mà quên đặt câu hỏi "ai được phép làm việc này, với dữ liệu của ai", bạn đang vô tình tạo ra một lỗ hổng trước cả khi dòng code đầu tiên được viết.
OWASP Top 10 là danh sách 10 nhóm rủi ro bảo mật web phổ biến và nguy hiểm nhất, do tổ chức phi lợi nhuận OWASP (Open Worldwide Application Security Project) tổng hợp từ dữ liệu hàng triệu ứng dụng thật. Nó không phải tài liệu kỹ thuật hàn lâm — nó là "bản đồ những cái bẫy" mà mọi sản phẩm phần mềm đều có nguy cơ rơi vào. Là Technical BA, bạn không cần biết cách khai thác lỗ hổng, nhưng bạn bắt buộc phải biết chúng tồn tại, để khi viết spec bạn cài sẵn các "chốt chặn" ngăn chúng xảy ra. Bài này sẽ giúp bạn nhìn OWASP Top 10 bằng con mắt của người viết yêu cầu, chứ không phải con mắt của hacker.
Khái niệm cốt lõi
OWASP Top 10 là gì và đọc nó như thế nào
OWASP Top 10 được cập nhật vài năm một lần. Phiên bản gần nhất sắp xếp các rủi ro theo mức độ phổ biến và tác động thực tế. Điều quan trọng với BA: mỗi mục không phải là một "lỗi code" đơn lẻ, mà là một nhóm rủi ro — và mỗi nhóm đều có gốc rễ ở một quyết định nghiệp vụ hoặc thiết kế nào đó. Dưới đây là cách hiểu 10 nhóm dưới góc nhìn requirement.
A01 — Broken Access Control (Kiểm soát truy cập hỏng)
Đây là rủi ro đứng đầu danh sách và cũng là nhóm mà BA ảnh hưởng nhiều nhất. Broken Access Control nghĩa là người dùng làm được những việc lẽ ra họ không được phép: xem dữ liệu của người khác, sửa thứ không thuộc về mình, hoặc truy cập chức năng dành cho admin.
Ví dụ kinh điển là IDOR (Insecure Direct Object Reference) — chính là tình huống mở đầu: đổi id trên URL để xem dữ liệu người khác. Gốc rễ không nằm ở code mà nằm ở chỗ requirement chỉ viết "người dùng xem được hóa đơn", mà quên mệnh đề sống còn: "hóa đơn của chính họ". Là BA, mỗi khi bạn viết một thao tác đọc/ghi dữ liệu, bạn phải tự hỏi: user này có quyền trên đúng bản ghi này không? — chứ không chỉ user này có đăng nhập chưa?
A02 — Cryptographic Failures (Lỗi mã hóa)
Trước đây nhóm này tên là "Sensitive Data Exposure". Nó bao gồm việc lưu mật khẩu dưới dạng plain text (chữ thường, đọc được trực tiếp trong database), gửi dữ liệu nhạy cảm qua HTTP thay vì HTTPS, hoặc dùng thuật toán mã hóa đã lỗi thời.
Với BA, điều cần nhớ: bạn phải phân loại dữ liệu ngay từ giai đoạn requirement. Trường nào là PII (thông tin định danh cá nhân — CMND/CCCD, số điện thoại), trường nào là dữ liệu tài chính, trường nào là bí mật (mật khẩu, token)? Mật khẩu không bao giờ được "mã hóa" — nó phải được hash (băm một chiều, không giải ngược được). Đây là chi tiết nhỏ nhưng phân biệt một spec chuyên nghiệp với một spec nghiệp dư.
A03 — Injection
Injection xảy ra khi dữ liệu người dùng nhập vào bị hệ thống "hiểu nhầm" thành câu lệnh. SQL Injection là loại nổi tiếng nhất: kẻ tấn công nhập ' OR '1'='1 vào ô tìm kiếm và lừa hệ thống trả về toàn bộ bảng dữ liệu. Ngoài ra còn có XSS (Cross-Site Scripting) — chèn mã JavaScript độc qua các ô nhập liệu.
BA không viết code chống injection, nhưng BA quyết định input nào được chấp nhận. Khi bạn định nghĩa rõ "trường số điện thoại chỉ nhận 10–11 chữ số", "trường tên không chứa thẻ HTML", bạn đang thu hẹp bề mặt tấn công.
A04 đến A10 — phần còn lại BA cần nắm
- A04 Insecure Design: lỗ hổng nằm ngay trong thiết kế nghiệp vụ, ví dụ quy trình "quên mật khẩu" cho phép thử OTP vô hạn lần. Đây là nhóm OWASP cố tình thêm vào để nhấn mạnh: bảo mật phải được thiết kế từ đầu, không vá sau.
- A05 Security Misconfiguration: cấu hình sai, để lộ trang admin mặc định, bật chế độ debug trên production.
- A06 Vulnerable Components: dùng thư viện cũ có lỗ hổng đã biết.
- A07 Authentication Failures: cho phép mật khẩu yếu, không khóa tài khoản sau nhiều lần đăng nhập sai.
- A08 Software & Data Integrity Failures: tin tưởng dữ liệu/cập nhật từ nguồn không xác thực.
- A09 Logging & Monitoring Failures: không ghi log đủ để phát hiện tấn công.
- A10 Server-Side Request Forgery (SSRF): lừa server gọi đến địa chỉ nội bộ.
Tình huống thực tế
Tình huống 1 — Ví điện tử và lỗ hổng IDOR (Broken Access Control)
Một fintech startup tại TP.HCM, tạm gọi là PayQuick, ra mắt tính năng lịch sử giao dịch. API được thiết kế là GET /api/v1/transactions/{transactionId}. Đội backend kiểm tra rất kỹ việc người dùng đã đăng nhập (có token hợp lệ), nhưng không kiểm tra giao dịch đó có thuộc về người đang gọi hay không.
Một người dùng tò mò phát hiện rằng chỉ cần thay số transactionId là xem được giao dịch của người khác. Vì ID tăng tuần tự (100001, 100002...), anh ta viết một script đơn giản quét từ 1 đến 500000 và thu thập được hơn 200.000 bản ghi giao dịch trong một đêm, bao gồm tên, số tiền và thời gian. Sự cố bị một blogger công nghệ phanh phui, và PayQuick mất gần ba tuần xử lý khủng hoảng truyền thông.
Diễn giải: Token hợp lệ chỉ trả lời câu hỏi "anh là ai?" (authentication), chứ không trả lời "anh được xem cái gì?" (authorization). Requirement gốc viết: "User xem được chi tiết giao dịch khi click vào." Nó thiếu mệnh đề kiểm tra quyền sở hữu.
Bài học rút ra: Mỗi khi spec có thao tác truy cập một bản ghi cụ thể qua ID, BA phải viết rõ acceptance criteria dạng: "Hệ thống PHẢI trả về 403/404 nếu giao dịch không thuộc về user đang đăng nhập." Đồng thời nên đề xuất dùng ID khó đoán (UUID) thay vì số tăng tuần tự — đây là quyết định thiết kế mà BA hoàn toàn có thể nêu trong spec.
Tình huống 2 — Lưu mật khẩu plain text tại sàn TMĐT (Cryptographic Failures)
Một sàn thương mại điện tử cỡ vừa ở khu vực Đông Nam Á, gọi là ShopNow, bị rò rỉ database sau một vụ tấn công vào năm thứ hai hoạt động. Điều khiến sự cố trở nên thảm họa không phải bản thân vụ rò rỉ, mà là việc toàn bộ 1,2 triệu mật khẩu được lưu dưới dạng plain text — đọc trực tiếp được. Vì nhiều người dùng dùng chung một mật khẩu cho cả email và ngân hàng, thiệt hại lan rộng ra ngoài phạm vi ShopNow.
Khi điều tra, người ta phát hiện trong tài liệu đặc tả ban đầu, trường mật khẩu chỉ được mô tả là password: string, không một dòng nào nói về việc phải hash. Đội dev thời kỳ đầu — vốn thiếu kinh nghiệm — đã hiểu đúng theo nghĩa đen: lưu chuỗi ký tự vào database.
Diễn giải: Đây là minh chứng kinh điển cho việc requirement im lặng cũng là một loại requirement — và là loại nguy hiểm nhất. Khi spec không nói gì về bảo mật dữ liệu nhạy cảm, đội phát triển sẽ chọn con đường đơn giản nhất.
Bài học rút ra: Trong mọi spec có trường nhạy cảm, BA nên có một mục "Data Sensitivity & Handling" ghi rõ: mật khẩu phải được hash bằng thuật toán mạnh (như bcrypt/argon2), không bao giờ lưu hay log dạng plain text; dữ liệu PII phải mã hóa khi lưu trữ; mọi truyền tải qua HTTPS. Một dòng spec đúng chỗ có thể đã ngăn được thảm họa 1,2 triệu tài khoản.
Tình huống 3 — Quy trình hoàn tiền và lỗ hổng thiết kế (Insecure Design)
Một ngân hàng số tại Việt Nam triển khai tính năng hoàn tiền (refund) cho giao dịch nạp thẻ điện thoại bị lỗi. Quy trình thiết kế: nếu nạp thẻ thất bại, hệ thống tự động hoàn tiền vào ví. Nghe hợp lý. Nhưng có một kẽ hở: trạng thái "thất bại" được xác định bằng phản hồi từ nhà mạng, và phản hồi này đôi khi đến chậm. Một nhóm người dùng phát hiện rằng nếu họ nạp thẻ rồi nhanh chóng ngắt mạng đúng thời điểm, hệ thống ghi nhận "thất bại" và hoàn tiền — trong khi thẻ thực tế đã được nạp thành công. Họ vừa có thẻ điện thoại, vừa được hoàn tiền.
Trong vài tuần, ngân hàng thất thoát một khoản tiền không nhỏ trước khi phát hiện qua đối soát.
Diễn giải: Không có dòng code nào "lỗi" theo nghĩa kỹ thuật. Lỗ hổng nằm ở thiết kế nghiệp vụ — đây chính là A04 Insecure Design. Requirement giả định "thất bại = chưa nạp", nhưng thực tế có trạng thái thứ ba: "không xác định".
Bài học rút ra: BA phải truy vấn các giả định ngầm trong luồng nghiệp vụ, đặc biệt ở những điểm liên quan đến tiền và trạng thái bất đồng bộ. Câu hỏi vàng: "Điều gì xảy ra nếu bước này thành công nhưng hệ thống tưởng là thất bại?" Hoàn tiền nên ràng buộc với đối soát xác nhận từ nhà mạng, không chỉ dựa vào timeout.
Hướng dẫn từng bước
Đây là quy trình thực hành để tích hợp tư duy OWASP vào công việc viết requirement của bạn.
Bước 1 — Phân loại dữ liệu trong phạm vi tính năng. Trước khi viết user story, liệt kê mọi trường dữ liệu liên quan và gắn nhãn: Công khai / Nội bộ / Nhạy cảm (PII) / Bí mật (mật khẩu, token, khóa). Việc gắn nhãn này quyết định mức độ bảo vệ cần đặc tả về sau.
Bước 2 — Với mỗi thao tác, đặt hai câu hỏi tách biệt. Câu một: "Ai được phép thực hiện?" (vai trò — authorization theo chức năng). Câu hai: "Trên dữ liệu của ai?" (quyền sở hữu bản ghi — chính là lá chắn chống IDOR). Đa số spec yếu chỉ trả lời câu một và bỏ quên câu hai.
Bước 3 — Viết acceptance criteria cho cả luồng "tiêu cực". Đừng chỉ mô tả happy path. Với mỗi story nhạy cảm, thêm tiêu chí kiểu: "Khi user A cố truy cập tài nguyên của user B, hệ thống trả về 403 và ghi log sự kiện." Luồng tiêu cực chính là nơi bảo mật được kiểm thử.
Bước 4 — Đặc tả xử lý input. Với mỗi trường nhập liệu, nêu rõ định dạng hợp lệ, độ dài tối đa, ký tự được phép. Điều này vừa giúp UX, vừa thu hẹp bề mặt injection.
Bước 5 — Đặc tả logging và thông báo lỗi. Yêu cầu ghi log cho các sự kiện bảo mật quan trọng (đăng nhập sai nhiều lần, truy cập trái phép), nhưng nhấn mạnh: log không được chứa mật khẩu, token, hay số thẻ đầy đủ. Đồng thời, thông báo lỗi cho người dùng phải mơ hồ một cách có chủ đích (ví dụ "Tài khoản hoặc mật khẩu không đúng" thay vì "Mật khẩu sai").
Bước 6 — Đưa security vào Definition of Done. Trước khi đóng một story nhạy cảm, có một checklist OWASP ngắn để rà. Và quan trọng nhất: mời chuyên gia security review spec ở những tính năng động đến tiền, danh tính, hoặc dữ liệu cá nhân — trước khi dev bắt đầu code.
Lỗi thường gặp & mẹo
Lỗi 1 — Nhầm authentication với authorization. Rất nhiều BA viết "user phải đăng nhập để xem" rồi coi như xong phần bảo mật. Đăng nhập chỉ chứng minh danh tính. Nó không tự động giới hạn user chỉ xem dữ liệu của mình. Hãy luôn tách bạch hai khái niệm này trong đầu.
Lỗi 2 — Để requirement "im lặng" về dữ liệu nhạy cảm. Như tình huống ShopNow, việc không nói gì về hash mật khẩu dẫn đến hậu quả thảm khốc. Mẹo: tạo một mục bảo mật mặc định trong template spec của bạn, để bạn buộc phải điền chứ không thể bỏ trống.
Lỗi 3 — Chỉ viết happy path. Spec chỉ mô tả khi mọi thứ đúng. Kẻ tấn công sống ở các nhánh sai. Mẹo: với mỗi story nhạy cảm, dành ít nhất một acceptance criteria cho hành vi khi bị lạm dụng.
Lỗi 4 — Dùng ID tuần tự cho tài nguyên nhạy cảm. ID kiểu 1, 2, 3 mời gọi việc dò quét. Mẹo: đề xuất UUID hoặc mã ngẫu nhiên cho các tài nguyên public-facing.
Lỗi 5 — Nghĩ "đó là việc của dev". Bảo mật là môn thể thao đồng đội, nhưng requirement là điểm xuất phát. Nếu cái bẫy đã có sẵn trong spec, dev giỏi đến mấy cũng khó cứu. Mẹo: coi OWASP Top 10 như một checklist đọc lướt mỗi khi viết tính năng mới.
Mẹo bonus — Học cách "đọc spec bằng con mắt kẻ xấu". Sau khi viết xong một user story, hãy đọc lại và tự hỏi: "Nếu tôi muốn phá hoại tính năng này, tôi sẽ lợi dụng kẽ hở nào?" Thói quen này biến bạn từ người ghi chép yêu cầu thành người bảo vệ sản phẩm.
Bài tập thực hành
Bài 1 — Vá lỗ hổng IDOR. Cho user story sau: "Là khách hàng, tôi muốn tải hóa đơn PDF của đơn hàng qua link /order/{orderId}/invoice để lưu trữ." Hãy viết lại story này kèm tối thiểu 3 acceptance criteria, trong đó có ít nhất một tiêu chí xử lý trường hợp user cố truy cập đơn hàng của người khác. Nêu rõ mã trạng thái HTTP trả về.
Bài 2 — Phân loại dữ liệu. Cho màn hình đăng ký gồm các trường: họ tên, email, số điện thoại, số CCCD, mật khẩu, mã giới thiệu. Hãy lập bảng phân loại từng trường theo 4 mức (Công khai / Nội bộ / Nhạy cảm / Bí mật) và ghi rõ cách xử lý lưu trữ tương ứng cho mỗi mức.
Bài 3 — Săn lỗ hổng thiết kế. Đọc luồng sau: "Người dùng nhập email để nhận mã OTP đặt lại mật khẩu. Nhập đúng OTP thì được tạo mật khẩu mới." Hãy chỉ ra ít nhất 3 rủi ro bảo mật tiềm ẩn (gợi ý: số lần thử OTP, thời hạn OTP, dò email tồn tại) và viết requirement bổ sung để khắc phục từng rủi ro.
Bài 4 — Tự kiểm tra. Lấy một spec gần nhất bạn từng viết (hoặc một tính năng quen thuộc), rà soát qua A01, A02, A03 của OWASP và viết một đoạn ngắn: tính năng đó có nguy cơ rơi vào nhóm nào, và bạn sẽ bổ sung gì.
Tóm tắt
OWASP Top 10 không phải tài liệu dành riêng cho dân kỹ thuật — nó là tấm bản đồ cảnh báo những cái bẫy phổ biến nhất mà mọi phần mềm có thể vướng, và phần lớn các bẫy đó bắt nguồn từ những quyết định ở tầng requirement, nơi BA làm chủ. Ba nhóm bạn phải khắc cốt ghi tâm là Broken Access Control (luôn tách bạch "ai là ai" với "ai được làm gì trên dữ liệu của ai"), Cryptographic Failures (đừng để spec im lặng về dữ liệu nhạy cảm, mật khẩu phải hash chứ không lưu thô), và Injection (kiểm soát chặt input). Bài học cốt lõi từ ba tình huống PayQuick, ShopNow và quy trình hoàn tiền ngân hàng đều giống nhau: lỗ hổng nguy hiểm nhất thường không phải bug code, mà là một mệnh đề bị bỏ quên trong yêu cầu. Là Technical BA, vũ khí của bạn không phải công cụ tấn công, mà là thói quen đặt câu hỏi đúng, viết acceptance criteria cho cả luồng tiêu cực, phân loại dữ liệu từ sớm, và kéo đội security vào trước khi code bắt đầu. Mỗi dòng spec đúng chỗ về bảo mật có thể tiết kiệm cho công ty hàng tuần khủng hoảng và hàng triệu dữ liệu người dùng.