Bài 24. Lỗ hổng ứng dụng: injection, buffer overflow, race condition, hardcoded secrets
Khóa học CompTIA Security+ SY0-801 · Phần 2: Mối đe dọa, lỗ hổng và tấn công · Bài 24 · 🎯 Objective 2.4 · ⏱️ Đọc khoảng 14 phút · 🧪 Không cần lab · 📝 5 câu bài tập cuối bài
🎯 Mục tiêu bài học
- Giải thích vì sao phần mềm có lỗ hổng và liên hệ với nguyên tắc "không tin dữ liệu đầu vào".
- Mô tả bốn nhóm lỗi: injection, buffer overflow, race condition và hardcoded secrets.
- Nhận biết thêm rò rỉ bộ nhớ (memory leak) và thành phần lỗi thời.
- Biết các biện pháp phòng tránh: kiểm tra đầu vào, ngôn ngữ an toàn bộ nhớ, khóa tài nguyên, quản lý bí mật.
- Biết các cách kiểm tra mã: review, phân tích tĩnh, phân tích động, fuzzing.
📎 Vị trí trong đề thi: từ bài này, Phần 2 chuyển sang lỗ hổng. Ở Bài 15 bạn đã học cách mô tả và chấm điểm lỗ hổng. Giờ ta xem chúng đến từ đâu. Đề thi hay mô tả một hiện tượng (nhập dữ liệu lạ làm ứng dụng sập, hai thao tác cùng lúc cho kết quả sai, mật khẩu nằm trong mã nguồn) rồi hỏi tên loại lỗi hoặc biện pháp khắc phục.
💡 Liên hệ thực tế: bốn hiện tượng trong phần mềm đơn hàng
Thứ Năm, 14:00. Công ty chị Lan dùng một phần mềm quản lý đơn hàng do đơn vị bên ngoài viết riêng. Nam được nhờ xem vì có vài hiện tượng lạ. Đây là tình huống minh họa, không phải vụ việc có thật.
- Ô tìm kiếm tên khách hàng báo lỗi kỳ lạ khi nhập một số ký tự đặc biệt, và lỗi hiện ra cả nội dung câu truy vấn.
- Một chức năng viết bằng ngôn ngữ cũ bị sập hẳn khi ai đó nhập một cái tên dài bất thường.
- Hai nhân viên cùng bấm "áp mã giảm giá" vào đúng một lúc, và mã dùng một lần lại được áp hai lần.
- Trong kho mã nguồn, Nam thấy mật khẩu cơ sở dữ liệu viết thẳng trong tệp cấu hình.
| Hiện tượng | Loại lỗi | Nam đề xuất |
|---|---|---|
| Ký tự đặc biệt gây lỗi truy vấn | Injection do không xử lý đầu vào | Kiểm tra đầu vào, dùng truy vấn tham số hóa |
| Nhập tên dài làm sập chức năng | Buffer overflow | Kiểm tra độ dài, cập nhật mã, bật bảo vệ bộ nhớ |
| Mã giảm giá bị áp hai lần | Race condition | Kiểm tra và đánh dấu trong một thao tác không chia cắt |
| Mật khẩu nằm trong mã nguồn | Hardcoded secrets | Chuyển sang kho bí mật, đổi mật khẩu ngay |
Chị Lan hỏi: "Mấy lỗi này đều do người viết ẩu hả em?" Nam trả lời: phần lớn là do giả định sai. Người viết tin rằng dữ liệu nhập vào luôn "đẹp", hai thao tác không bao giờ trùng lúc, và không ai xem được mã nguồn. Kẻ tấn công tìm đúng những giả định đó.
📘 Kiến thức cốt lõi
🧱 Vì sao ứng dụng có lỗ hổng?
Phần mềm là do con người viết, chạy trong môi trường phức tạp. Nhiều lỗ hổng bắt nguồn từ một vài nguyên nhân chung:
- Tin dữ liệu đầu vào (độ dài, ký tự, định dạng) mà không kiểm tra.
- Giả định về thứ tự và thời điểm các thao tác diễn ra.
- Lưu thông tin nhạy cảm sai chỗ, ví dụ trong mã nguồn.
- Dùng thành phần cũ hoặc có lỗi mà không biết (Bài 20 đã nói về SBOM).
Nguyên tắc nền tảng: không bao giờ tin dữ liệu đầu vào. Mọi thứ từ người dùng, tệp, mạng hay hệ thống khác đều phải được kiểm tra trước khi dùng.
💉 Injection
Injection xảy ra khi ứng dụng nhận dữ liệu của người dùng rồi ghép vào một lệnh, và bộ xử lý hiểu phần dữ liệu đó như một phần của lệnh. Kẻ tấn công nhờ vậy làm cho ứng dụng thực hiện điều nó không được thiết kế để làm.
Một số dạng thường gặp:
| Dạng | Lệnh bị chèn vào | Ví dụ ngắn |
|---|---|---|
| SQL injection | Câu truy vấn cơ sở dữ liệu | Ô tìm kiếm bị biến thành câu lệnh lấy toàn bộ bảng (chi tiết ở Bài 25) |
| Command injection | Lệnh của hệ điều hành | Chức năng "kiểm tra kết nối" bị lợi dụng để chạy lệnh khác |
| LDAP, XML... | Truy vấn hoặc tài liệu cấu trúc khác | Ô đăng nhập bị lợi dụng để sửa truy vấn thư mục người dùng |
Cách phòng chống chung:
- Kiểm tra và làm sạch đầu vào (input validation, sanitization): chỉ chấp nhận định dạng mong đợi, ưu tiên danh sách cho phép.
- Tách dữ liệu khỏi lệnh: dùng truy vấn tham số hóa thay vì ghép chuỗi.
- Quyền tối thiểu cho tài khoản mà ứng dụng dùng để truy cập hệ thống.
- Không hiển thị lỗi chi tiết cho người dùng cuối, chỉ ghi vào nhật ký.
🧨 Buffer overflow
Buffer overflow (tràn bộ đệm) xảy ra khi chương trình ghi dữ liệu nhiều hơn dung lượng vùng nhớ được cấp. Phần dư ghi đè lên vùng nhớ kế bên, có thể chứa dữ liệu hay thông tin điều khiển của chương trình.
🧨 Hình 24a. Bộ đệm chứa được 8 ký tự, nhưng đầu vào dài 12 ký tự đã tràn sang 4 ô kế bên.
- Hậu quả: chương trình sập, hoạt động sai, hoặc trong trường hợp xấu hơn, kẻ tấn công điều khiển được chương trình chạy mã của họ.
- Dễ gặp ở ngôn ngữ để lập trình viên tự quản lý bộ nhớ như C và C++. Các ngôn ngữ có kiểm tra giới hạn tự động (nhiều ngôn ngữ hiện đại) giảm rủi ro này đáng kể.
Cách phòng chống:
- Kiểm tra độ dài đầu vào trước khi ghi vào bộ nhớ, dùng hàm có kiểm tra giới hạn.
- Dùng ngôn ngữ an toàn bộ nhớ khi có thể.
- Bảo vệ ở cấp hệ điều hành: DEP (đánh dấu vùng dữ liệu là không thể chạy mã) và ASLR (xáo trộn địa chỉ bộ nhớ mỗi lần chạy), khiến việc khai thác khó hơn.
- Vá lỗi và cập nhật thư viện.
Có một lỗi liên quan: rò rỉ bộ nhớ (memory leak). Chương trình cấp phát bộ nhớ nhưng không trả lại, dùng lâu dần hết tài nguyên và làm ứng dụng chậm hoặc sập. Nó không cho phép chạy mã của kẻ tấn công, nhưng có thể bị lợi dụng để gây từ chối dịch vụ.
⏱️ Race condition
Race condition (điều kiện tranh đua) xảy ra khi kết quả phụ thuộc vào thứ tự hay thời điểm của hai hoặc nhiều thao tác chạy đồng thời. Dạng điển hình trong đề thi là TOCTOU (time of check to time of use): giữa lúc kiểm tra điều kiện và lúc sử dụng kết quả, điều kiện đã bị đổi.
⏱️ Hình 24b. Cả hai cùng thấy "mã còn dùng được" trước khi ai kịp đánh dấu đã dùng.
- Hậu quả: áp dụng ưu đãi nhiều lần, rút tiền vượt số dư, vượt qua kiểm tra quyền, ghi đè tệp được kiểm tra.
- Khó phát hiện vì chỉ xảy ra ở một số thời điểm, khó tái hiện.
Cách phòng chống:
- Gộp kiểm tra và hành động thành một thao tác không chia cắt (atomic operation), ví dụ giao dịch cơ sở dữ liệu.
- Khóa (lock) tài nguyên dùng chung để mỗi lúc chỉ một luồng xử lý.
- Kiểm thử với nhiều thao tác đồng thời.
🔐 Hardcoded secrets
Hardcoded secrets (bí mật viết cứng) là mật khẩu, khóa API, mã thông báo hoặc khóa riêng được ghi thẳng trong mã nguồn hoặc tệp cấu hình đi kèm ứng dụng.
- Mã nguồn thường được chia sẻ rộng (đồng nghiệp, nhà thầu, kho mã), và có thể bị lộ nhầm lên kho công khai.
- Ngay cả ứng dụng đã biên dịch cũng có thể bị phân tích ngược để lấy chuỗi bí mật.
- Một khi bí mật lộ, mọi bản triển khai đều dùng chung nó, nên khó thu hồi.
Cách phòng chống:
- Lưu bí mật ở nơi riêng: kho quản lý bí mật (secrets manager, vault) hoặc cấu hình được bảo vệ, ứng dụng lấy khi chạy.
- Quét mã nguồn để phát hiện bí mật bị lỡ ghi vào (kể cả trong lịch sử thay đổi).
- Xoay vòng (rotate) bí mật định kỳ. Nếu đã từng lộ, phải đổi ngay, vì chỉ xóa khỏi mã là chưa đủ: bản cũ vẫn còn trong lịch sử kho mã.
- Quyền tối thiểu cho mỗi khóa hay tài khoản.
🔎 Kiểm tra mã nguồn và ứng dụng
| Phương pháp | Ý nghĩa |
|---|---|
| Rà soát mã (code review) | Người khác đọc mã để tìm lỗi trước khi phát hành |
| Phân tích tĩnh (SAST) | Công cụ quét mã nguồn khi chưa chạy |
| Phân tích động (DAST) | Công cụ kiểm tra ứng dụng đang chạy từ bên ngoài |
| Fuzzing | Tự động đưa vào rất nhiều dữ liệu bất thường để xem ứng dụng có sập hay xử lý sai không |
| Quét thành phần phụ thuộc | Tìm thư viện đã biết có lỗ hổng (liên hệ Bài 20) |
Việc đưa các bước này vào quy trình phát triển được gọi là phát triển phần mềm an toàn (secure SDLC). Phát hiện lỗi càng sớm càng rẻ để sửa.
🎯 Mẹo làm bài: nhìn hiện tượng. "Dữ liệu nhập bị hiểu như lệnh" là injection. "Nhập quá dài làm sập hoặc ghi đè" là buffer overflow. "Hai thao tác cùng lúc cho kết quả sai" hoặc "điều kiện đổi giữa lúc kiểm tra và lúc dùng" là race condition (TOCTOU). "Mật khẩu hay khóa trong mã nguồn" là hardcoded secrets. "Tự động đưa dữ liệu ngẫu nhiên, bất thường" là fuzzing.
🧠 Áp vào câu chuyện của Nam
| Câu hỏi của chị Lan | Khái niệm trả lời |
|---|---|
| "Vì sao ô tìm kiếm báo lỗi hiện cả câu truy vấn?" | Injection và lộ thông tin lỗi chi tiết |
| "Nhập tên dài mà sập là sao?" | Buffer overflow do không kiểm tra độ dài |
| "Hai người bấm cùng lúc mà mã dùng hai lần?" | Race condition, cần thao tác không chia cắt |
| "Mật khẩu trong mã nguồn thì làm sao?" | Hardcoded secrets: chuyển vào kho bí mật, đổi mật khẩu |
| "Làm sao phát hiện sớm những lỗi kiểu này?" | Code review, SAST, DAST, fuzzing |
📖 Thuật ngữ cần nhớ
| Thuật ngữ | Nghĩa |
|---|---|
| Input validation | Kiểm tra dữ liệu đầu vào trước khi dùng. |
| Injection | Dữ liệu người dùng bị hiểu như một phần của lệnh. |
| Buffer overflow | Ghi vượt dung lượng vùng nhớ, ghi đè vùng kế bên. |
| DEP / ASLR | Bảo vệ bộ nhớ: không chạy mã trong vùng dữ liệu / xáo trộn địa chỉ. |
| Memory leak | Không trả lại bộ nhớ, làm cạn tài nguyên. |
| Race condition | Kết quả phụ thuộc thứ tự và thời điểm các thao tác. |
| TOCTOU | Điều kiện đổi giữa lúc kiểm tra và lúc sử dụng. |
| Atomic operation | Thao tác không thể chia cắt. |
| Hardcoded secrets | Mật khẩu, khóa viết cứng trong mã nguồn. |
| SAST / DAST | Phân tích tĩnh mã nguồn / phân tích động ứng dụng đang chạy. |
| Fuzzing | Đưa dữ liệu bất thường tự động để tìm lỗi. |
📌 Tóm tắt nhanh
- Nhiều lỗ hổng bắt nguồn từ giả định sai: tin đầu vào, tin thứ tự thao tác, giấu bí mật trong mã.
- Injection: dữ liệu bị hiểu như lệnh. Chống bằng kiểm tra đầu vào, tách dữ liệu khỏi lệnh, quyền tối thiểu.
- Buffer overflow: ghi vượt vùng nhớ. Chống bằng kiểm tra độ dài, ngôn ngữ an toàn bộ nhớ, DEP và ASLR.
- Race condition (TOCTOU): chống bằng thao tác không chia cắt và khóa.
- Hardcoded secrets: dùng kho bí mật, quét mã, xoay vòng. Đã lộ thì phải đổi.
💬 Hỏi & Đáp nhanh
❓ Xóa mật khẩu khỏi mã nguồn là đủ chưa? Chưa. Các phiên bản cũ của mã vẫn nằm trong lịch sử kho mã và có thể đã được sao chép. Phải đổi mật khẩu hoặc khóa đó, rồi chuyển sang cách lưu an toàn.
❓ Buffer overflow có còn phổ biến không? Vẫn còn trong phần mềm viết bằng ngôn ngữ quản lý bộ nhớ thủ công và trong phần mềm cũ, phần sụn thiết bị. Ngôn ngữ hiện đại và các biện pháp như DEP, ASLR giúp giảm nhưng không loại bỏ hoàn toàn.
❓ SAST và DAST khác nhau thế nào? SAST đọc mã nguồn khi chưa chạy, nên tìm được lỗi sớm và chỉ ra đúng dòng mã. DAST tấn công thử ứng dụng đang chạy từ bên ngoài, nên thấy được lỗi về cấu hình và môi trường nhưng không biết dòng mã nào gây ra. Hai cách bổ sung cho nhau.
📝 Bài tập đánh giá
Tự chọn đáp án cho từng câu, rồi đối chiếu với phần đáp án ở cuối bài.
Câu 1. Nguyên nhân gốc rễ của lỗi injection là gì?
- A. Ứng dụng dùng quá nhiều bộ nhớ
- B. Ứng dụng ghép dữ liệu người dùng vào lệnh mà không xử lý đúng nên dữ liệu bị hiểu như một phần của lệnh
- C. Mạng quá chậm
- D. Người dùng đặt mật khẩu quá ngắn
Câu 2. Một chương trình nhận đầu vào dài hơn vùng nhớ dành cho nó và ghi đè lên dữ liệu kế bên. Đây là lỗi gì?
- A. Race condition
- B. Memory leak
- C. Buffer overflow
- D. Hardcoded secrets
Câu 3. Hai thao tác đồng thời cùng thấy một mã giảm giá "chưa dùng" và cùng áp mã đó. Cách khắc phục phù hợp nhất là gì?
- A. Thêm màu sắc cho giao diện
- B. Gộp kiểm tra và đánh dấu đã dùng thành một thao tác không chia cắt hoặc khóa tài nguyên
- C. Tăng độ dài mã giảm giá
- D. Gửi email nhắc nhân viên
Câu 4. Mật khẩu cơ sở dữ liệu bị phát hiện viết thẳng trong mã nguồn đã đưa lên kho mã. Hành động nào đúng nhất?
- A. Chỉ xóa dòng đó khỏi mã nguồn
- B. Đổi mật khẩu ngay, chuyển bí mật vào kho quản lý bí mật và quét lịch sử mã
- C. Giữ nguyên vì kho mã là nội bộ
- D. Đổi tên tệp cấu hình
Câu 5. Kỹ thuật nào tự động đưa vào ứng dụng rất nhiều dữ liệu bất thường để tìm lỗi như sập hoặc xử lý sai?
- A. Phân tích lưu lượng
- B. Fuzzing
- C. Sao lưu
- D. Phân đoạn mạng
👉 Đáp án
Câu 1: B — Injection xảy ra khi dữ liệu của người dùng bị ghép vào lệnh và được hiểu như một phần của lệnh.
Câu 2: C — Ghi vượt dung lượng bộ đệm và ghi đè vùng nhớ kế bên là buffer overflow.
Câu 3: B — Gộp kiểm tra và hành động thành thao tác không chia cắt, hoặc dùng khóa, loại bỏ khoảng hở giữa lúc kiểm tra và lúc dùng.
Câu 4: B — Phải đổi bí mật đã lộ, chuyển sang kho quản lý bí mật và kiểm tra lịch sử mã. Chỉ xóa khỏi mã là chưa đủ.
Câu 5: B — Fuzzing đưa dữ liệu bất thường tự động để phát hiện lỗi.
📚 Bài viết thuộc khóa học CompTIA Security+ của VNExperts