Bảo mật Kiến trúc hệ thống

Thứ ba, 28 Th07 2026 · 12 phút đọc · 9 lượt xem

Xóa mù lỗ hổng web (Phần 1): Khi dữ liệu người dùng bị hiểu nhầm thành câu lệnh

Xóa mù lỗ hổng web (Phần 1): Khi dữ liệu người dùng bị hiểu nhầm thành câu lệnh

Gần như mọi lỗ hổng nguy hiểm của ứng dụng web đều quay về một sai lầm rất cũ: hệ thống không phân biệt được đâu là dữ liệu do người dùng gửi lên và đâu là câu lệnh của chính nó. Khách hàng gõ tên vào ô tìm kiếm, nhưng cơ sở dữ liệu lại đọc chuỗi đó như một mệnh lệnh. Một người để lại bình luận, nhưng trình duyệt của người khác lại chạy nó như JavaScript.

Trong OWASP Top 10:2025 — phiên bản mới nhất, thay thế bản 2021 mà phần lớn tài liệu tiếng Việt vẫn đang trích dẫn — nhóm Injection xếp ở vị trí A05, nhưng vẫn là nhóm có số CVE lớn nhất: hơn 62.000 CVE trải trên 37 CWE, trong đó riêng Cross-site Scripting đã hơn 30.000 và SQL Injection hơn 14.000. Nói cách khác, nó tụt hạng không phải vì bớt nguy hiểm, mà vì các nhóm khác đã vượt lên.

Bài viết này dành cho lập trình viên mới vào nghề, QA và PM muốn hiểu bản chất lỗ hổng để biết mình đang bảo vệ điều gì.

2025-mappings.pngBả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. SQL Injection — kinh điển nhưng chưa hề chết

Ứng dụng nhận tham số từ URL rồi ghép thẳng vào câu truy vấn:

String query = "SELECT * FROM accounts WHERE custID='" + request.getParameter("id") + "'";

Khi kẻ tấn công gửi lên một chuỗi chứa dấu nháy kèm mệnh đề logic luôn đúng, cấu trúc câu lệnh bị bẻ gãy và truy vấn trả về toàn bộ bảng thay vì một bản ghi. Hậu quả có thể là đọc trộm dữ liệu, sửa, xóa, hoặc gọi stored procedure.

Một hiểu lầm phổ biến: dùng ORM không tự động an toàn. OWASP nêu rõ trường hợp HQL của Hibernate vẫn dính lỗi nếu lập trình viên nối chuỗi thay vì truyền tham số.

Cách phòng chống

Ưu tiên số một là tham số hóa truy vấn (prepared statement) hoặc dùng API an toàn của ORM — dữ liệu đi một đường, cấu trúc câu lệnh đi một đường:

PreparedStatement ps = conn.prepareStatement("SELECT * FROM accounts WHERE custID = ?");
ps.setString(1, request.getParameter("id"));

Bổ sung thêm các lớp phòng thủ: kiểm tra đầu vào theo danh sách cho phép (allowlist) ở phía server; tuyệt đối không để người dùng quyết định tên bảng, tên cột hay hướng sắp xếp vì đây là những thứ không thể escape; tài khoản cơ sở dữ liệu của ứng dụng chỉ nên có quyền tối thiểu; và bật quét SAST/DAST trong CI/CD để bắt lỗi trước khi lên production. WAF chỉ là lớp giảm thiểu, không thay thế được việc viết truy vấn đúng.

sql-injection-infographic.pngCơ chế của một cuộc tấn công SQL Injection. Nguồn: Cloudflare Learning Center.

2. OS Command Injection — nguy hiểm hơn SQL Injection nhiều

Khi ứng dụng ghép đầu vào người dùng vào một lệnh hệ điều hành — ví dụ chức năng kiểm tra tên miền gọi tới nslookup — kẻ tấn công chỉ cần thêm ký tự phân tách lệnh là chạy được lệnh tùy ý trên máy chủ. Đây là con đường ngắn nhất dẫn tới việc mất toàn bộ server.

Phòng chống: tránh gọi shell nếu đã có thư viện làm được việc đó; nếu buộc phải gọi tiến trình con, hãy truyền tham số dưới dạng mảng đối số thay vì một chuỗi lệnh và không đi qua shell; validate đầu vào bằng allowlist chặt chẽ; chạy tiến trình với quyền thấp nhất trong container bị giới hạn.

3. Cross-Site Scripting (XSS) — mã độc chạy trong trình duyệt người dùng

XSS xảy ra khi nội dung do người dùng nhập được nhả ra HTML mà không được mã hóa. Có ba dạng chính: stored (mã độc nằm sẵn trong cơ sở dữ liệu, ai xem cũng dính), reflected (nằm trong đường link gửi cho nạn nhân) và DOM-based (do chính JavaScript phía client ghi dữ liệu vào DOM). Hậu quả điển hình là đánh cắp phiên đăng nhập, giả mạo thao tác của người dùng, hoặc chèn form đăng nhập giả.

Cách phòng chống

Nguyên tắc gốc là mã hóa đầu ra theo đúng ngữ cảnh: dữ liệu nhả vào HTML body, vào thuộc tính, vào JavaScript hay vào URL đều cần cách escape khác nhau. Các framework hiện đại như React, Vue, Angular hay Blade đã escape mặc định; vấn đề nằm ở chỗ lập trình viên chủ động phá rào bằng innerHTML, v-html hay dangerouslySetInnerHTML. Nếu bắt buộc phải hiển thị HTML do người dùng nhập, hãy làm sạch bằng thư viện chuyên dụng như DOMPurify thay vì tự viết biểu thức chính quy lọc thẻ.

Hai lớp bổ sung rất đáng giá: khai báo Content-Security-Policy để chặn script không hợp lệ ngay cả khi đã lọt lỗi, và gắn thuộc tính HttpOnly cho cookie phiên để JavaScript không đọc được token.

xss-attack.pngLuồng tấn công Cross-Site Scripting. Nguồn: Cloudflare Learning Center.

4. SSRF — bắt máy chủ của bạn đi lấy dữ liệu hộ kẻ tấn công

Server-Side Request Forgery xảy ra khi ứng dụng nhận một URL từ người dùng rồi tự gửi request tới đó: chức năng nhập ảnh từ URL, webhook, hay bộ chuyển HTML sang PDF. Kẻ tấn công thay bằng địa chỉ nội bộ để moi những thứ mà từ Internet không chạm tới được — dịch vụ nội bộ, endpoint metadata của cloud chứa credential tạm thời, hoặc trang quản trị chỉ mở trong mạng riêng.

Đáng chú ý: trong bản 2025, OWASP đã gộp SSRF vào nhóm A01 Broken Access Control. Cách phân loại này nói đúng bản chất — đây là lỗi vượt qua ranh giới tin cậy.

Phòng chống: chỉ cho phép gọi tới danh sách domain hoặc IP đã được duyệt, không dùng blocklist; phân giải DNS rồi kiểm tra IP đích trước khi kết nối để chặn các dải nội bộ; không tự động đi theo redirect; tách phần fetch ra một service riêng nằm trong subnet không có quyền truy cập nội bộ và kiểm soát egress ở tầng network.

5. Path traversal và tải file lên

Hai chức năng tưởng vô hại nhưng thường bị bỏ quên. Với chức năng tải file xuống theo tên do người dùng chỉ định, chuỗi vượt cấp cho phép đi ngược ra khỏi thư mục dự kiến để đọc file cấu hình hoặc khóa bí mật. Với chức năng tải file lên, rủi ro là kẻ tấn công đưa lên một file thực thi rồi tìm cách gọi nó.

Phòng chống: không bao giờ ghép tên file do người dùng cung cấp vào đường dẫn — hãy sinh tên ngẫu nhiên và lưu ánh xạ trong cơ sở dữ liệu; chuẩn hóa đường dẫn rồi kiểm tra nó vẫn nằm trong thư mục gốc cho phép; xác định loại file bằng nội dung thật thay vì tin phần mở rộng hay Content-Type; lưu file ở object storage riêng, phục vụ qua domain khác và không cho phép thực thi trong thư mục upload.

6. Prompt injection — phiên bản 2026 của bài toán cũ

Nếu sản phẩm của bạn có chatbot hoặc pipeline RAG, hãy biết rằng OWASP đã tách riêng LLM01:2025 Prompt Injection trong bộ OWASP Top 10 for LLM. Bản chất giống hệt injection truyền thống: mô hình không phân biệt được đâu là chỉ dẫn của hệ thống, đâu là dữ liệu người dùng hay nội dung tài liệu được nạp vào ngữ cảnh. Nguy hiểm hơn là dạng gián tiếp, khi chỉ dẫn độc hại nằm sẵn trong tài liệu mà bot đi truy xuất.

Hướng giảm thiểu: coi mọi đầu ra của mô hình là dữ liệu không tin cậy, không đưa thẳng vào SQL, shell hay HTML; giới hạn quyền của các công cụ mà agent được phép gọi theo nguyên tắc tối thiểu; bắt buộc con người xác nhận với những hành động có tác động thật; đánh dấu rõ ranh giới giữa chỉ dẫn hệ thống và nội dung truy xuất; ghi log toàn bộ prompt và tool call để còn điều tra khi có sự cố.

Checklist trước khi merge một pull request

  • Mọi truy vấn cơ sở dữ liệu đều được tham số hóa, không còn chỗ nối chuỗi.
  • Không có đầu vào người dùng nào được ghép vào lệnh hệ điều hành.
  • Mọi dữ liệu hiển thị ra giao diện đều được mã hóa theo đúng ngữ cảnh.
  • Đã khai báo Content-Security-Policy và cookie phiên có HttpOnly.
  • Mọi URL do người dùng cung cấp đều đi qua allowlist trước khi server gọi tới.
  • Đường dẫn file được chuẩn hóa và kiểm tra nằm trong thư mục cho phép.
  • Validate chạy ở phía server, không chỉ ở form phía client.
  • CI đã có bước quét SAST và quét phụ thuộc.

Kết

Toàn bộ phần một quy về một câu: giữ dữ liệu tách khỏi câu lệnh, và không bao giờ tin đầu vào từ phía client. Phần hai sẽ nói về nhóm lỗ hổng khó nhìn thấy hơn nhưng gây thiệt hại lớn hơn — phân quyền, xác thực, cấu hình sai và chuỗi cung ứng phần mềm.

Nguồn tham khảo

Sẵn sàng chuyển đổi doanh nghiệp?

Hãy thảo luận về cách chúng tôi có thể giúp bạn tận dụng AI và chuyển đổi số.

Chia sẻ