Áp chót của sê-ri, và là bài tôi nghĩ có tỷ lệ "công bỏ ra trên rủi ro tránh được" cao nhất.

Bắt đầu bằng một phép quét mà bạn chạy được trong ba phút trên chính dự án của mình.

Năm dòng phụ thuộc, bảy mươi ba lỗ hổng

Tôi lấy đúng pom.xml đã dùng ở bài 91 — năm phụ thuộc, toàn thư viện phổ biến — và quét bằng OSV-Scanner:

docker run --rm -v $(pwd):/src ghcr.io/google/osv-scanner:latest scan --lockfile=/src/pom.xml
  Scanned /src/pom.xml file and found 5 packages

  Total 27 packages affected by 73 known vulnerabilities
  (6 Critical, 32 High, 27 Medium, 8 Low)
  66 vulnerabilities can be fixed.

Năm phụ thuộc khai ra, hai mươi bảy gói dính lỗ hổng.

Sáu lỗ hổng nghiêm trọng:

  log4j:log4j 1.2.17
    GHSA-2qrg-x229-3v8q — Deserialization of Untrusted Data in Log4j
    GHSA-65fg-84f6-3jq3 — SQL Injection in Log4j 1.2.x
    GHSA-f7vh-qwp3-x37m — Deserialization of Untrusted Data in Apache Log4j
  org.apache.avro:avro 1.7.7
    GHSA-r7pg-v2c8-mfg3 — Arbitrary Code Execution when reading Avro Data
  org.apache.zookeeper:zookeeper 3.6.3
    GHSA-7286-pgfv-vxvh — Authorization Bypass Through User-Controlled Key
  org.codehaus.jackson:jackson-mapper-asl 1.9.13
    GHSA-c27h-mcmw-48hv — Deserialization of Untrusted Data

Ba điều đáng nói về danh sách này.

Không cái nào do tôi khai. Tôi khai httpclient, hadoop-common, lombok, junit, logback. Sáu lỗ hổng nghiêm trọng đều đến bắc cầu, chủ yếu qua hadoop-common. Bài 91 đã cho thấy cây phụ thuộc sâu tới mức nào; đây là hệ quả bảo mật của điều đó.

log4j:log4j 1.2.17 là bản 1.x — ngừng hỗ trợ từ 2015. Nó vẫn nằm trong cây phụ thuộc của những thư viện rất phổ biến năm 2026. Và đây không phải Log4Shell (lỗi đó ở 2.x); đây là ba lỗ hổng khác của một thư viện đã chết mười một năm.

Ba trong sáu là deserialization — đúng chủ đề bài 59, nơi tôi cho thấy readObject chạy mã trước khi bạn kịp kiểm tra gì. Nó vẫn là họ lỗ hổng lớn nhất của nền tảng.

Con số 73 không có nghĩa ứng dụng của bạn bị khai thác được ở 73 chỗ — phần lớn lỗ hổng cần một đường dẫn cụ thể mà mã của bạn có thể không đi qua. Nhưng nó là danh sách việc cần rà, và không có nó thì bạn thậm chí không biết chúng tồn tại.

Công cụ quét

OSV-Scanner — nhanh, chạy bằng một lệnh Docker, không cần khoá API. Đây là cái tôi dùng ở trên và là cái tôi khuyên bắt đầu.

OWASP Dependency-Check — dùng nhiều nhất trong doanh nghiệp, có plugin Maven:

<plugin>
  <groupId>org.owasp</groupId><artifactId>dependency-check-maven</artifactId>
  <version>10.0.3</version>
  <configuration><failBuildOnCVSS>7</failBuildOnCVSS></configuration>
</plugin>

failBuildOnCVSS là phần quan trọng: làm build đỏ khi có lỗ hổng nghiêm trọng, thay vì sinh một báo cáo mà không ai đọc. Lưu ý bản mới cần khoá API của NVD, và lần chạy đầu tải cơ sở dữ liệu khá lâu.

mvn dependency:tree để tìm ai kéo thư viện đó vào — bước bắt buộc sau khi quét, vì bạn không sửa được thứ mình không khai. Cách xử lý là dependencyManagement để ghim phiên bản mới, hoặc <exclusions> rồi khai bản sạch.

Và điều quan trọng nhất: chạy nó trong CI, không phải một lần rồi thôi. Lỗ hổng mới được công bố mỗi ngày cho thư viện bạn đã dùng từ lâu; mã không đổi mà rủi ro vẫn tăng.

XXE: đọc trọn tệp hệ thống bằng một tệp XML

Đây là lỗ hổng tôi thấy ít được nhắc trong tài liệu tiếng Việt, dù cấu hình mặc định của Java có lỗ.

<?xml version="1.0"?>
<!DOCTYPE hoso [ <!ENTITY bimat SYSTEM "file:///etc/passwd"> ]>
<hoso><ten>&bimat;</ten></hoso>

Phân tích bằng DocumentBuilderFactory mặc định:

  mặc định  -> ĐỌC ĐƯỢC 887 ký tự | dòng đầu: root:x:0:0:root:/root:/bin/bash

Nội dung /etc/passwd chui vào phần tử <ten>, và nếu ứng dụng của bạn hiển thị lại giá trị đó thì kẻ tấn công vừa đọc được tệp trên máy chủ. Đổi đường dẫn thành một URL nội bộ là thành SSRF.

Cách chặn:

DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
f.setFeature("http://xml.org/sax/features/external-general-entities", false);
f.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
f.setXIncludeAware(false);
f.setExpandEntityReferences(false);
  có chặn -> chặn: DOCTYPE is disallowed when the feature "..." set to true

Dòng đầu tiên là dòng quan trọng nhất — cấm hẳn DOCTYPE chặn được cả họ tấn công này, kể cả "quả bom tỷ tiếng cười" làm cạn bộ nhớ.

Áp dụng cho mọi bộ phân tích XML bạn dùng: SAXParserFactory, XMLInputFactory, TransformerFactory, SchemaFactory, XPathFactory. Mỗi cái có cách cấu hình riêng, và mặc định của chúng đều không an toàn.

Một chút công bằng: các khung làm việc hiện đại thường đã chặn sẵn — Spring cấu hình cứng phần này. Rủi ro nằm ở chỗ bạn tự viết một chỗ đọc XML.

Những cái đã gặp trong sê-ri

Bốn lỗ hổng lớn nhất đều đã có bài riêng, nên tôi chỉ nhắc lại kèm bài học:

SQL injection (bài 65). Tôi cho một chuỗi 28 ký tự vào ô nhập và xoá cả một bảng, không ngoại lệ nào được ném ra. Chữa: mọi giá trị từ bên ngoài đi qua dấu ?, không có ngoại lệ. Tên bảng và tên cột không thay bằng ? được — phải đối chiếu với danh sách cho phép.

Deserialization (bài 59). readObject chạy mã của lớp trước khi bạn cầm được đối tượng, nên "kiểm tra dữ liệu trước khi dùng" là vô nghĩa. Chữa: đừng dùng Java serialization cho dữ liệu không tin cậy; nếu buộc phải thì ObjectInputFilter với danh sách cho phép.

XSS qua JSON nhúng vào HTML (bài 60). Jackson không thoát <> — đúng theo chuẩn JSON — nên một tiêu đề chứa </script> đóng sớm thẻ script. Chữa: thoát thành < khi nhúng JSON vào trang.

Rò dữ liệu qua ThreadLocal (bài 78). Request thứ tư đọc ra tên của người dùng ở request thứ hai vì luồng trong pool mang theo giá trị cũ. Chữa: remove() trong finally, ở tầng sở hữu vòng đời request.

Bốn chỗ khác đáng kiểm

Duyệt đường dẫn. new File(thuMuc, tenDoNguoiDungNhap) với tên là ../../etc/passwd sẽ đi ra khỏi thư mục bạn định. Chữa bằng cách chuẩn hoá rồi kiểm:

Path goc = Path.of("/du-lieu").toRealPath();
Path dich = goc.resolve(ten).normalize();
if (!dich.startsWith(goc)) throw new SecurityException("đường dẫn không hợp lệ");

normalize() một mình không đủ — phải có bước startsWith sau đó.

Mật mã yếu. MD5SHA-1 không dùng để băm mật khẩu; SHA-256 cũng không, vì nó quá nhanh. Dùng bcrypt, scrypt hay Argon2. Và Random không dùng cho token — phải là SecureRandom, vì Random đoán được sau vài mẫu.

Chuyển hướng mở. Bài học đã ghi trong CLAUDE.md của chính blog này: chuyển hướng theo Referer phải chỉ lấy phần đường dẫn và bắt buộc bắt đầu bằng đúng một dấu gạch chéo — //evil.com/x là URL theo giao thức tương đối, trình duyệt đi thẳng ra ngoài.

Thông báo lỗi lộ nội tình. Trả e.getMessage() của DataIntegrityViolationException ra ngoài là lộ nguyên câu SQL và tên ràng buộc. Ghi chi tiết vào log, trả ra ngoài một thông điệp chung.

Ba việc làm được ngay hôm nay

Chạy một lần quét. Một lệnh Docker, ba phút. Bảng ở đầu bài là kết quả của đúng lệnh đó trên một pom.xml năm dòng.

Đưa nó vào CI với ngưỡng làm đỏ build. Báo cáo không ai đọc thì không bảo vệ được gì.

Bật cập nhật phụ thuộc tự động — Dependabot hoặc Renovate. Phần lớn lỗ hổng trong bảng trên chỉ cần nâng phiên bản, và 66 trong 73 cái được đánh dấu là sửa được.

Ba việc đó không đòi hỏi hiểu biết bảo mật sâu, và chúng xử lý được phần lớn rủi ro thực tế. Còn những lỗ hổng trong mã của chính bạn thì cần đọc kỹ hơn — nhưng phụ thuộc bên ngoài mới là chỗ đông người bị nhất, đơn giản vì không ai nhìn tới.

Thử ba mươi giây

docker run --rm -v $(pwd):/src ghcr.io/google/osv-scanner:latest scan --lockfile=/src/pom.xml

Không cần cài gì, không cần khoá API. Con số hiện ra ở dòng "Total ... vulnerabilities" là thứ đang có trong ứng dụng của bạn ngay lúc này — và nếu nó lớn hơn bạn tưởng, đó chính là điểm của bài này.

Ngày mai là bài cuối cùng: nhìn lại một trăm ngày, và bản đồ đi tiếp từ đây.