Nhiều engineer xem OWASP Top 10 như checklist cần “học thuộc”, SQL injection, XSS, CSRF, broken authentication, xong. Mỗi sprint có ai đó hỏi “security review chưa?”, check qua danh sách, đánh dấu “đã xử lý”, và move on. Cho đến khi một API endpoint bị khai thác qua SSRF, attacker gửi URL nội bộ qua form import, server fetch metadata AWS, lấy được credential. OWASP có SSRF trong danh sách, ai cũng biết SSRF tồn tại, nhưng không nghĩ endpoint import file lại là attack surface.

Vấn đề không phải thiếu kiến thức OWASP, vấn đề là cách suy nghĩ về security. Checklist tĩnh không nói cho bạn biết attack surface cụ thể của feature đang build. Cái cần là tư duy theo luồng dữ liệu: dữ liệu không tin cậy đi vào đâu, quyền được kiểm tra ở đâu, attacker có khả năng gì tại từng lớp.

Bài này hướng dẫn cách threat model cho feature mới, không phải pentest methodology đầy đủ, mà là framework thực dụng cho dev khi implement feature có dữ liệu nhạy cảm hoặc tiếp nhận input từ bên ngoài.


Luồng dữ liệu và trust boundary

Trước khi check bất kỳ item nào trong OWASP, nên vẽ luồng dữ liệu, đơn giản nhất là ASCII diagram:

[Browser/Mobile] --TLS--> [Ingress/LB] --> [API Service] --> [DB]
      untrusted           terminate TLS     policy check      trusted-ish

Trust boundary là ranh giới bạn không tin input: user input, webhook từ partner, header từ client, file upload. Mỗi lần dữ liệu cắt qua boundary, phải có validate + authentication/authorization rõ ràng.

“Tin middleware” mà không biết middleware làm gì là lỗ hổng quy trình. Hay gặp: team tin rằng “API gateway đã authn rồi” nên handler không check lại, khi gateway config sai cho một path, handler expose data cho unauthenticated request.

Vẽ luồng giúp bạn thấy mọi điểm dữ liệu không tin cậy chạm vào hệ thống. Checklist OWASP sau đó dùng để kiểm tra từng điểm đó, có hướng, có mục tiêu, thay vì scan qua 10 item generic.


STRIDE: checklist có hướng, không phải bảng đánh dấu

STRIDE là framework phân loại threat, mỗi chữ cái là một loại threat. Dùng nó như checklist hỏi tại mỗi trust boundary, không phải “đánh dấu đủ ô”.

Spoofing, Ai gọi được endpoint này? JWT/session có verify đúng issuer không? Clock skew có bị exploit không? Hay gặp: JWT verify bỏ qua exp check vì “clock server lệch 5 phút”, attacker dùng token hết hạn 1 giờ.

Tampering, Client có thể sửa userId trong body để đọc data người khác không? Form hidden field có tin được không? Input validation ở server hay chỉ client?

Repudiation, Có audit log cho hành động nhạy cảm không? User xóa data rồi nói “tôi không xóa”, có chứng cứ không?

Information Disclosure, Lỗi 500 có leak stack trace, database schema không? Log có chứa PII? API response có trả thêm field mà client không cần (over-fetching sensitive data)?

Denial of Service, Endpoint có thể bị spam expensive query không? Có pagination, rate limit không? File upload có giới hạn size?

Elevation of Privilege, Role admin có guard ở mọi path hay chỉ ở UI? Mass assignment: framework có cho user set role: "admin" qua JSON binding không?

STRIDE không thay thế authz matrix test, nó giúp bạn nhớ hỏi đúng câu hỏi tại mỗi boundary.


Bốn họ lỗ hổng theo điểm giao cắt

Thay vì đi qua 10 item OWASP, nhóm theo điểm giao cắt trong code, nơi dữ liệu không tin cậy chạm vào hệ thống.

Injection

Giao cắt: mọi chỗ ghép string vào query, shell command, template. SQL injection kinh điển nhưng còn NoSQL injection, OS command injection, template injection (SSTI).

Kiểm soát tối thiểu: parameterized query (không bao giờ ghép string), allow-list input cho command/path, không eval dữ liệu user. Đây là loại dễ phòng nhất nếu dùng đúng tool, ORM với prepared statement gần như miễn nhiễm.

SSRF

Giao cắt: URL do user cung cấp mà server fetch, import file, webhook callback, image proxy. Attacker gửi URL nội bộ (169.254.169.254 metadata, localhost:6379 Redis) → server fetch, trả data internal.

Kiểm soát tối thiểu: allow-list domain, chặn private IP ranges, egress network policy. Nhưng cẩn thận: attacker bypass bằng redirect chain, DNS rebinding, hoặc URL parser inconsistency giữa validator và HTTP client. Tầng bổ sung: proxy egress có allow-list, disable follow redirect, giới hạn hop count.

Broken Authorization

Giao cắt: mọi handler đọc/ghi resource theo tenantId, orderId, userId. Đây là loại phổ biến nhất trong thực tế, và cũng là loại nguy hiểm nhất vì thường lọt qua code review.

Kiểm soát tối thiểu: authorization ở server theo resource, không tin client role flag. Tạo authz matrix (bảng role × endpoint × allow/deny) và viết test tự động cho mỗi ô “deny”. Bug “đọc được đơn hàng người khác” thường lọt vì thiếu test deny.

Deserialization

Giao cắt: nhận blob phức tạp từ nguồn không tin cậy, file upload, webhook payload, queue message. Java deserialization, Python pickle, PHP unserialize đều có gadget chain exploit.

Kiểm soát tối thiểu: dùng format đơn giản (JSON) với schema validation, giới hạn kích thước, không deserialize format phức tạp từ untrusted source.


Defense-in-depth: tier hóa theo rủi ro

Không phải mọi service đều cần WAF enterprise + HSM + pentest hàng tháng. Tier hoá kiểm soát theo dữ liệu:

Tier 0, dữ liệu công khai (blog, static content): validate input, rate limit nhẹ. Đủ.

Tier 1, PII user (profile, email, address): tất cả T0 + authn mạnh (MFA nếu cần), authz theo resource, log redact PII, dependency scanning.

Tier 2, tiền/thanh toán: tất cả T1 + audit trail đầy đủ, fraud signals, change review hai người, pentest định kỳ, secret rotation tự động.

OWASP ASVS gợi ý “level” tương tự, bạn không cần Level 3 cho endpoint health check. Tier hóa giúp team tập trung effort vào đúng chỗ thay vì “max security everywhere” mà thực tế không ai follow.


Webhook từ đối tác: ví dụ luồng chi tiết

Đây là case thực tế hay cần threat model, partner gửi webhook POST /webhooks/partner với JSON ký HMAC.

Bước 1: TLS terminate ở ingress. Kiểm tra cert, cipher phù hợp. Nếu partner gửi qua HTTP → reject.

Bước 2: Verify signature với clock skew có giới hạn. HMAC với shared secret, timestamp trong header, reject nếu timestamp quá cũ (chống replay). Nonce nếu partner hỗ trợ.

Bước 3: Authorization, webhook này thuộc tenant nào? Đừng tin field tenant_id trong body nếu không có registry secret per tenant. Mỗi partner/tenant có secret riêng → verify signature = verify identity.

Bước 4: Handler idempotent theo event_id đối tác, partner có thể retry webhook khi không nhận 200. Handler phải xử lý duplicate mà không tạo side-effect thêm.

Mỗi bước là cơ hội fail-closed (reject nếu nghi ngờ) hoặc fail-open (chấp nhận để không miss event). Chọn sai kiểu, ví dụ skip verify khi load cao, là chấp nhận rủi ro, phải document và có monitoring.


Mass assignment: threat ẩn trong framework

Framework cho phép bind JSON body → model object dễ vô tình cho user set field sensitive. Gửi {"name": "Alice", "role": "admin"}, nếu framework auto-bind tất cả field, user tự cấp admin role.

Kiểm soát: allow-list field explicit (DTO riêng cho public API), tách schema “admin update” khỏi “user self-service”. Đừng dùng model entity trực tiếp cho API binding.


Rate limit như threat control

Abuse không chỉ DDoS volumetric, credential stuffing (thử password hàng loạt), scraping data, brute force OTP. Rate limit theo IP + user ID + device fingerprint (tùy privacy policy).

Rate limit không chắc chắn chặn được mọi attacker, nó là chi phí cho attacker, mỗi rate limit layer tăng cost để attack. Attacker cần nhiều IP hơn, rotate credential chậm hơn, exploit tốn thời gian hơn.

Rate limit là phần của threat model, không phải afterthought. Mỗi endpoint nhận untrusted input nên có rate limit, mức độ strict tùy tier.


File upload: trust giảm dần theo từng bước

File đi qua: browser → API → object storage → antivirus worker → CDN. Mỗi bước có threat riêng:

Browser → API: MIME type spoofing (rename .exe thành .jpg), path traversal trong filename, size limit.

API → storage: không lưu file trên filesystem với tên user cung cấp (path traversal), dùng random key, content-type sniffing server-side.

Storage → antivirus: scan async, quarantine until clean, reject nếu scan fail (fail-closed cho T2 data).

Vẽ trust boundary theo từng bước thay vì một hộp “upload”, mỗi bước có validate và sanitize riêng.


Supply chain: dependency là attack surface

Threat modeling hiện đại không thể bỏ qua npm/pip/maven, một package trong dependency graph có thể là điểm vào. Event-stream incident (2018), ua-parser-js (2021), colors.js (2022) đều là ví dụ thực tế.

Kiểm soát tối thiểu: lockfile + CI verify checksum (đảm bảo package không bị swap giữa install và build). Dependabot/Renovate có policy SLA merge (security patch merge trong 48h). Không chạy postinstall script tùy tiện trong CI context có access secret.


Bảo mật log

{ "msg": "login failed", "user": "user@example.com", "ip": "1.2.3.4" }

Log này cần cho fraud detection, nhưng retention dài + search rộng tạo rủi ro GDPR. Policy phù hợp: hash user identifier (one-way, đủ để join nếu cần investigate), partial IP (bỏ octet cuối), retention 30 ngày cho log loại này.

Log lỗi 500 có thể leak stack trace chứa database table name, query structure, internal path, không nên trả cho client. Trả error code generic, log chi tiết internal.


Authz matrix: test tối thiểu

Đây là nơi bug “đọc được đơn hàng người khác” thường lọt, và cũng là nơi ít team viết test nhất.

Tạo bảng nhỏ:

RoleGET /orders/:idPATCH /orders/:idDELETE /orders/:id
Ownerallowallowallow
Other userdenydenydeny
Adminallowallow? (define clearly)

Viết ít nhất một test tự động cho mỗi ô “deny”. Test authz quan trọng hơn test business logic trong nhiều trường hợp, business logic sai thì user thấy lỗi, authz sai thì data leak.


Template threat model một trang

Template một trang cho mỗi feature mới có trust boundary:

System: [tên feature/service]
Assets: [dữ liệu gì quan trọng, PII, payment, credential]
Adversaries: [ai có motivation, external attacker, malicious user, insider]
Entry points: [endpoint, webhook, file upload, queue consumer]
Top 5 risks: [STRIDE-based, ranked by impact × likelihood]
Controls: [kiểm soát cho mỗi risk, cụ thể, không "validate input"]
Test plan: [authz matrix test, injection test, rate limit test]
Reviewers: [ai review, security champion, tech lead]

Một trang buộc bạn ưu tiên, mười trang thành shelfware không ai đọc. Top 5 risks thay vì “mọi risk” giúp team tập trung vào điều quan trọng nhất.


Khi không cần formal threat model

Spike nội bộ dưới 48 giờ: vẫn nên check boundary + injection cơ bản, nhưng không cần template đầy đủ.

Team đã có security champion review theo risk tier: champion biết pattern, review PR cho security-sensitive change đủ rồi, không cần document nặng.

Endpoint chỉ serve static content: rate limit + basic input validation đủ. Đừng làm threat model 5 trang cho health check endpoint.


Tóm tắt

Vẽ luồng dữ liệu và trust boundary trước khi check OWASP, biết data không tin cậy đi vào đâu quan trọng hơn học thuộc 10 item.

STRIDE là framework hỏi đúng câu hỏi tại mỗi boundary, Spoofing, Tampering, Repudiation, Information Disclosure, DoS, Elevation. Không phải đánh dấu đủ ô mà phải hiểu threat cụ thể cho feature cụ thể.

Tier hóa kiểm soát theo dữ liệu: T0 (public) chỉ cần cơ bản, T2 (tiền) cần full stack. Không “max security mọi nơi”, tốn effort mà không ai follow.

Authz matrix test cho mỗi ô “deny” là nơi bug nguy hiểm nhất lọt qua. Test authz quan trọng hơn nhiều thứ khác mà team thường viết test.

Supply chain (dependency), logging (PII), mass assignment (framework binding) là những threat hay bị bỏ qua vì không nằm trong “business code”. Đừng bỏ qua.


Tham khảo