Chuyển tới nội dung chính

Secure Coding Practices

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Nắm được nguyên tắc Defensive Programming trong Java
  • Biết cách phòng chống SQL Injection với PreparedStatement
  • Hiểu nguy cơ của Deserialization và cách phòng tránh
  • Áp dụng nguyên tắc Least Privilege trong thiết kế ứng dụng
  • Validate input đúng cách ở mọi tầng ứng dụng

Nguyên tắc Defensive Programming

Defensive Programming là viết code luôn giả định rằng input có thể sai, hệ thống có thể bị tấn công. Thay vì tin tưởng data, chúng ta luôn kiểm tra.

💡 Hình dung: Defensive Programming giống như lái xe phòng thủ — bạn không chỉ tuân thủ luật giao thông (validate input), mà còn luôn giả định xe khác có thể vi phạm (never trust external data). Bạn đeo dây an toàn (error handling), kiểm tra gương (logging), và có nhiều lớp bảo vệ (airbag, ABS, ESP = Defense in Depth).

5 nguyên tắc cốt lõi

#Nguyên tắcMô tả
1Never trust inputMọi input từ bên ngoài đều phải validate
2Least PrivilegeChỉ cấp quyền tối thiểu cần thiết
3Defense in DepthBảo vệ nhiều tầng, không dựa vào 1 điểm
4Fail SecurelyKhi lỗi xảy ra, hệ thống phải ở trạng thái an toàn
5Keep it SimpleCode đơn giản → ít lỗ hổng hơn

Input Validation

Tại sao Input Validation quan trọng?

Hầu hết các lỗ hổng bảo mật bắt nguồn từ việc không validate input: SQL Injection, XSS, Path Traversal, Command Injection... Tất cả đều khai thác input không được kiểm tra.

Validation Strategy

InputValidator.java
public class InputValidator {

/**
* Validate username: chỉ chứa alphanumeric và underscore, 3-20 ký tự.
*/
public static String validateUsername(String input) {
if (input == null || input.isBlank()) {
throw new IllegalArgumentException("Username không được để trống");
}
// Whitelist approach — chỉ cho phép ký tự hợp lệ
if (!input.matches("^[a-zA-Z0-9_]{3,20}$")) {
throw new IllegalArgumentException(
"Username chỉ chứa a-z, 0-9, _ và dài 3-20 ký tự");
}
return input;
}

/**
* Validate email.
*/
public static String validateEmail(String input) {
if (input == null || input.isBlank()) {
throw new IllegalArgumentException("Email không được để trống");
}
// Regex đơn giản cho email
if (!input.matches("^[\\w.+-]+@[\\w-]+\\.[a-zA-Z]{2,}$")) {
throw new IllegalArgumentException("Email không hợp lệ");
}
return input.toLowerCase().trim();
}

/**
* Validate và sanitize file path — chống Path Traversal.
*/
public static String validateFilePath(String input, String allowedDir) {
if (input == null || input.isBlank()) {
throw new IllegalArgumentException("File path không được để trống");
}
// Chống Path Traversal: ../../../etc/passwd
java.nio.file.Path resolved = java.nio.file.Path.of(allowedDir)
.resolve(input)
.normalize();

if (!resolved.startsWith(allowedDir)) {
throw new SecurityException("Path traversal detected: " + input);
}
return resolved.toString();
}
}
Whitelist vs Blacklist
  • Whitelist (khuyến nghị): Chỉ cho phép ký tự/pattern hợp lệ → ^[a-zA-Z0-9]+$
  • Blacklist (yếu): Chặn ký tự nguy hiểm → Dễ bỏ sót, attacker luôn tìm cách bypass
// ❌ Blacklist — dễ bị bypass
if (input.contains("'") || input.contains("--")) { reject(); }

// ✅ Whitelist — chỉ cho phép ký tự hợp lệ
if (!input.matches("^[a-zA-Z0-9 ]{1,100}$")) { reject(); }

Input Validation Decision Tree

Validate ở đâu?

┌─────────────────────────────────────────┐
│ Client (Browser/Mobile) │ ← UX validation (có thể bypass)
├─────────────────────────────────────────┤
│ Controller / API Layer │ ← Format validation (required, length, regex)
├─────────────────────────────────────────┤
│ Service / Business Layer │ ← Business rules validation
├─────────────────────────────────────────┤
│ Data Access Layer │ ← Parameterized queries
├─────────────────────────────────────────┤
│ Database │ ← Constraints (NOT NULL, CHECK)
└─────────────────────────────────────────┘

SQL Injection Prevention

SQL Injection là gì?

SQL Injection xảy ra khi attacker chèn SQL code vào input, thay đổi logic của query.

// ❌ VULNERABLE — String concatenation
String query = "SELECT * FROM users WHERE username = '" + username + "'";

// Attacker nhập: ' OR '1'='1' --
// Query thành: SELECT * FROM users WHERE username = '' OR '1'='1' --'
// → Trả về TẤT CẢ users!

PreparedStatement — Giải pháp

SQLInjectionPrevention.java
import java.sql.*;

public class SQLInjectionPrevention {

// ❌ VULNERABLE — String concatenation
public static void unsafeQuery(Connection conn, String username)
throws SQLException {
Statement stmt = conn.createStatement();
// KHÔNG BAO GIỜ làm thế này!
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
ResultSet rs = stmt.executeQuery(sql);
}

// ✅ SAFE — PreparedStatement
public static void safeQuery(Connection conn, String username)
throws SQLException {
String sql = "SELECT * FROM users WHERE username = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username); // Tự động escape
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
System.out.println("User: " + rs.getString("username"));
}
}
}
}

// ✅ SAFE — PreparedStatement với nhiều parameters
public static void safeInsert(Connection conn, String username,
String email, int age) throws SQLException {
String sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username);
pstmt.setString(2, email);
pstmt.setInt(3, age);
int rows = pstmt.executeUpdate();
System.out.println("Inserted " + rows + " row(s)");
}
}

// ✅ SAFE — Dynamic column name (whitelist approach)
public static void safeDynamicQuery(Connection conn, String sortColumn)
throws SQLException {
// Whitelist các column được phép sort
var allowedColumns = java.util.Set.of("username", "email", "created_at");
if (!allowedColumns.contains(sortColumn)) {
throw new IllegalArgumentException("Invalid sort column: " + sortColumn);
}

String sql = "SELECT * FROM users ORDER BY " + sortColumn;
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
// Process results
}
}
}

SQL Injection Attack vs Parameterized Query Defense

PreparedStatement không bảo vệ mọi thứ

PreparedStatement chỉ bảo vệ values (dữ liệu). Với table names, column names, và ORDER BY, bạn phải dùng whitelist validation.

// PreparedStatement KHÔNG hỗ trợ:
// ❌ Table name as parameter
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM ?");
// ❌ Column name as parameter
PreparedStatement pstmt = conn.prepareStatement("SELECT ? FROM users");

Injection Attack Types Overview

XSS Prevention (Cross-Site Scripting)

XSS xảy ra khi attacker chèn JavaScript vào trang web, thực thi trong browser của nạn nhân.

Loại XSSMô tảVí dụ
ReflectedInput phản hồi ngay trong responseSearch query hiển thị trên trang
StoredPayload lưu trong DB, hiển thị cho user khácComment chứa script
DOM-basedXử lý input phía client không an toàninnerHTML = userInput
XSSPrevention.java
// ❌ VULNERABLE — output trực tiếp user input
response.getWriter().write("<p>Hello, " + username + "</p>");
// Attacker: username = "<script>alert('XSS')</script>"

// ✅ SAFE — dùng OWASP Java Encoder
import org.owasp.encoder.Encode;
response.getWriter().write(
"<p>Hello, " + Encode.forHtml(username) + "</p>");

// ✅ SAFE — Content Security Policy header
response.setHeader("Content-Security-Policy",
"default-src 'self'; script-src 'self'");

LDAP Injection

Tương tự SQL Injection nhưng nhắm vào LDAP queries:

// ❌ VULNERABLE — string concatenation
String filter = "(uid=" + username + ")";
// Attacker: username = "*)(|(uid=*)"
// Filter thành: (uid=*)(|(uid=*)) → trả về TẤT CẢ users!

// ✅ SAFE — escape special characters
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;

String safeUsername = Rdn.escapeValue(username);
String filter = "(uid=" + safeUsername + ")";

Command Injection

// ❌ VULNERABLE — Runtime.exec() với string concatenation
Runtime.getRuntime().exec("ping " + userInput);
// Attacker: userInput = "google.com; rm -rf /"

// ✅ SAFE — ProcessBuilder với tách command và arguments
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", hostname);
pb.redirectErrorStream(true);
Process process = pb.start();

// ✅ SAFE — validate input trước
if (!hostname.matches("^[a-zA-Z0-9.-]+$")) {
throw new SecurityException("Invalid hostname");
}

Serialization Security

Nguy cơ của Java Serialization

Java Serialization (ObjectInputStream) là một trong những attack vector nguy hiểm nhất — cho phép attacker thực thi code tuỳ ý (Remote Code Execution).

// ❌ DANGEROUS — Deserialize untrusted data
ObjectInputStream ois = new ObjectInputStream(untrustedInput);
Object obj = ois.readObject(); // Có thể chạy code độc!

Tại sao nguy hiểm?

Khi deserialize, Java gọi các method đặc biệt (readObject(), readResolve()...) trên object. Attacker có thể tạo gadget chain — chuỗi các class có sẵn trong classpath mà khi deserialize sẽ dẫn đến RCE.

Giải pháp

SafeDeserialization.java
import java.io.*;

public class SafeDeserialization {

// ✅ Giải pháp 1: ObjectInputFilter (Java 9+)
public static Object safeDeserialize(InputStream input) throws Exception {
ObjectInputStream ois = new ObjectInputStream(input);

// Chỉ cho phép deserialize các class cụ thể
ois.setObjectInputFilter(filterInfo -> {
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
// Whitelist — chỉ cho phép String, Integer, và các class an toàn
if (clazz == String.class ||
clazz == Integer.class ||
clazz.getName().startsWith("com.myapp.dto.")) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
}
// Giới hạn depth và size
if (filterInfo.depth() > 5) return ObjectInputFilter.Status.REJECTED;
if (filterInfo.references() > 100) return ObjectInputFilter.Status.REJECTED;
return ObjectInputFilter.Status.UNDECIDED;
});

return ois.readObject();
}

// ✅ Giải pháp 2: Dùng JSON thay vì Java Serialization
// Jackson, Gson — an toàn hơn nhiều
// objectMapper.readValue(json, MyDTO.class);

// ✅ Giải pháp 3: Record (Java 16+) — immutable, không có readObject()
record UserDTO(String name, int age) implements Serializable {}
}

Secure Serialization/Deserialization Decision Process

Tránh Java Serialization cho external data

Khuyến nghị từ Oracle và OWASP: Không dùng ObjectInputStream để deserialize data từ nguồn không tin cậy. Thay thế bằng:

  • JSON: Jackson, Gson
  • Protocol Buffers: Google Protobuf
  • XML: JAXB (với XXE protection)

Secure Exception Handling

Không lộ thông tin nhạy cảm trong error message

// ❌ Sai — lộ thông tin hệ thống
try {
Connection conn = DriverManager.getConnection(url, user, pass);
} catch (SQLException e) {
// Trả về chi tiết lỗi cho client
return "Error: " + e.getMessage();
// → "Error: Access denied for user 'admin'@'10.0.0.5' (using password: YES)"
// → Attacker biết: username là admin, IP database là 10.0.0.5
}

// ✅ Đúng — log chi tiết, trả client thông báo chung
try {
Connection conn = DriverManager.getConnection(url, user, pass);
} catch (SQLException e) {
logger.error("Database connection failed", e); // Log chi tiết cho dev
return "Lỗi hệ thống. Vui lòng thử lại sau."; // Client chỉ thấy thông báo chung
}

Fail Securely

// ❌ Sai — khi lỗi → cho phép truy cập (fail open)
public boolean isAuthorized(String token) {
try {
return tokenService.validate(token);
} catch (Exception e) {
return true; // Lỗi → cho qua!
}
}

// ✅ Đúng — khi lỗi → từ chối truy cập (fail closed)
public boolean isAuthorized(String token) {
try {
return tokenService.validate(token);
} catch (Exception e) {
logger.warn("Token validation failed", e);
return false; // Lỗi → từ chối
}
}

Least Privilege

Áp dụng trong code

LeastPrivilegeExample.java
import java.util.Collections;
import java.util.List;

public class LeastPrivilegeExample {

// ❌ Sai — trả về mutable list, caller có thể modify
public List<String> getUserRoles() {
return this.roles; // Direct reference!
}

// ✅ Đúng — trả về immutable copy
public List<String> getUserRoles() {
return Collections.unmodifiableList(this.roles);
// Hoặc: return List.copyOf(this.roles);
}

// ❌ Sai — method quá rộng quyền
public void processData(Object data) {
// Nhận bất kỳ Object nào
}

// ✅ Đúng — type cụ thể, giới hạn input
public void processUserData(UserDTO userData) {
// Chỉ nhận UserDTO
}
}

Áp dụng cho Database Connection

// ❌ Sai — dùng root/admin account cho mọi thứ
Connection conn = DriverManager.getConnection(url, "root", "rootpass");

// ✅ Đúng — account riêng cho từng service, quyền tối thiểu
// App user chỉ có: SELECT, INSERT, UPDATE trên các bảng cần thiết
// KHÔNG có: DROP, ALTER, GRANT
Connection conn = DriverManager.getConnection(url, "app_readonly", "limited_pass");

Immutable Objects for Security

Immutable objects an toàn hơn vì không thể bị sửa đổi sau khi tạo:

// ✅ Record — immutable by default
public record UserDTO(String name, String email, List<String> roles) {
public UserDTO {
// Defensive copy — ngăn caller sửa list sau khi tạo
roles = List.copyOf(roles);
}
}

// ✅ List.copyOf() — tạo unmodifiable copy
List<String> original = new ArrayList<>(List.of("ADMIN", "USER"));
List<String> safe = List.copyOf(original);
original.add("HACKER"); // Không ảnh hưởng safe
// safe.add("HACKER"); // UnsupportedOperationException

Sensitive Data Handling

Dùng char[] thay vì String cho mật khẩu

// ❌ Sai — String nằm trong String Pool, không thể xoá
String password = "MyPassword123";
// password sẽ ở trong memory cho đến khi GC thu hồi

// ✅ Đúng — char[] có thể xoá ngay sau khi dùng
char[] password = new char[]{'M', 'y', 'P', 'a', 's', 's'};
try {
authenticate(password);
} finally {
java.util.Arrays.fill(password, '\0'); // Xoá khỏi memory
}
Tại sao JPasswordField.getPassword() trả về char[] ?

Swing JPasswordField trả về char[] thay vì String chính vì lý do bảo mật này. String immutable nằm trong String Pool và có thể bị đọc từ memory dump.

Không log dữ liệu nhạy cảm

// ❌ Sai — log mật khẩu
logger.info("User login: " + username + ", password: " + password);
logger.debug("Credit card: " + cardNumber);

// ✅ Đúng — mask dữ liệu nhạy cảm
logger.info("User login: {}", username);
logger.debug("Credit card: ****{}", cardNumber.substring(cardNumber.length() - 4));

Ví dụ thực tế: Secure User Registration

SecureUserService.java
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.sql.*;
import java.util.Base64;

public class SecureUserService {

private final Connection connection;

public SecureUserService(Connection connection) {
this.connection = connection;
}

/**
* Đăng ký user mới — áp dụng tất cả nguyên tắc secure coding.
*/
public void register(String username, char[] password, String email)
throws Exception {
try {
// 1. Input Validation (whitelist)
validateUsername(username);
validateEmail(email);
validatePassword(password);

// 2. Hash password với salt
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
byte[] hash = hashPassword(password, salt);

// 3. Dùng PreparedStatement (chống SQL Injection)
String sql = "INSERT INTO users (username, email, password_hash, salt) " +
"VALUES (?, ?, ?, ?)";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
pstmt.setString(1, username);
pstmt.setString(2, email);
pstmt.setString(3, Base64.getEncoder().encodeToString(hash));
pstmt.setString(4, Base64.getEncoder().encodeToString(salt));
pstmt.executeUpdate();
}
} finally {
// 4. Xoá password khỏi memory
java.util.Arrays.fill(password, '\0');
}
}

private void validateUsername(String username) {
if (username == null || !username.matches("^[a-zA-Z0-9_]{3,20}$")) {
throw new IllegalArgumentException("Username không hợp lệ");
}
}

private void validateEmail(String email) {
if (email == null || !email.matches("^[\\w.+-]+@[\\w-]+\\.[a-zA-Z]{2,}$")) {
throw new IllegalArgumentException("Email không hợp lệ");
}
}

private void validatePassword(char[] password) {
if (password == null || password.length < 8) {
throw new IllegalArgumentException("Password phải ít nhất 8 ký tự");
}
}

private byte[] hashPassword(char[] password, byte[] salt) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(salt);
return md.digest(new String(password).getBytes("UTF-8"));
}
}

Lỗi thường gặp

Lỗi thường gặp

1. String concatenation trong SQL:

// ❌ SQL Injection!
stmt.executeQuery("SELECT * FROM users WHERE id = " + userId);

// ✅ PreparedStatement
pstmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
pstmt.setInt(1, userId);

2. Deserialize dữ liệu không tin cậy:

// ❌ RCE vulnerability!
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object obj = ois.readObject();

// ✅ Dùng JSON parser
UserDTO user = objectMapper.readValue(request.getInputStream(), UserDTO.class);

3. Trả chi tiết lỗi cho client:

// ❌ Lộ thông tin
catch (Exception e) { return e.getStackTrace(); }

// ✅ Thông báo chung
catch (Exception e) { logger.error("Error", e); return "Internal error"; }
OCP Trap — PreparedStatement chỉ parameterize VALUES

PreparedStatement chỉ cho phép parameterize values (dữ liệu), KHÔNG parameterize table names, column names, hoặc ORDER BY clause. Đề thi OCP thường cho code dùng ? cho table name và hỏi kết quả.

// ❌ KHÔNG hoạt động — table name không thể parameterize
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM ? WHERE id = ?");
ps.setString(1, "users"); // SQLException!
ps.setInt(2, 1);

// ✅ Whitelist cho dynamic table/column names
Set<String> allowedTables = Set.of("users", "orders", "products");
if (!allowedTables.contains(tableName)) {
throw new SecurityException("Invalid table: " + tableName);
}
String sql = "SELECT * FROM " + tableName + " WHERE id = ?";
OCP Trap — char[] vs String cho passwords

String nằm trong String Pool và không thể xoá khỏi memory cho đến khi GC thu hồi. char[] có thể zero-fill ngay sau khi dùng.

// ❌ String — tồn tại trong memory không kiểm soát được
String password = "secret";
// password nằm trong String Pool, có thể bị đọc từ heap dump

// ✅ char[] — có thể xoá ngay
char[] password = {'s','e','c','r','e','t'};
try {
authenticate(password);
} finally {
Arrays.fill(password, '\0'); // Zero-fill
}

Đây là lý do JPasswordField.getPassword() trả về char[] thay vì String, và PBEKeySpec nhận char[].

📖 Oracle Secure Coding Guidelines

Oracle khuyến nghị 9 nguyên tắc Secure Coding cho Java:

  1. Guideline 0: Input validation — Validate tất cả input từ untrusted sources
  2. Guideline 1: Minimize accessibility — Dùng access modifiers restrictive nhất
  3. Guideline 2: Defensive copying — Copy mutable objects khi nhận và trả
  4. Guideline 5: Serialization — Tránh hoặc kiểm soát chặt
  5. Guideline 6: Sensitive data — Dùng char[] thay String cho passwords

Tham khảo: Oracle Secure Coding Guidelines for Java SE

Bài tập

Bài 1: Input Validation Library [Cơ bản]

Xây dựng class Validator với các method static:

  • validateUsername(String) — 3-20 ký tự, alphanumeric + underscore
  • validateEmail(String) — email format hợp lệ
  • validateAge(int) — 0-150
  • validateFilePath(String, String baseDir) — chống path traversal Mỗi method throw ValidationException khi input không hợp lệ.
Gợi ý

Dùng regex cho username và email. Dùng Path.normalize()startsWith() cho file path.

Bài 2: Secure DAO Pattern [Trung bình]

Implement UserDAO với:

  • findByUsername(String) — tìm user (PreparedStatement)
  • create(UserDTO) — tạo user với password hashing
  • updateEmail(int userId, String newEmail) — validate + update
  • search(String keyword, String sortBy) — tìm kiếm với whitelist sort columns Tất cả phải dùng PreparedStatement và input validation.
Gợi ý

Tạo interface UserDAO và class SecureUserDAO implements nó. Dùng try-with-resources cho tất cả database resources.

Bài 3: Security Audit Tool [Thách thức]

Viết tool scan source code Java để phát hiện:

  • String concatenation trong SQL query (pattern: "SELECT.*" +)
  • Sử dụng ObjectInputStream không có filter
  • Catch block trống (catch (Exception e) {})
  • Hardcoded passwords (pattern: password = "...") Tool nhận đường dẫn folder, scan tất cả file .java và báo cáo findings.
Gợi ý

Dùng Files.walk() để traverse folder, Pattern regex để tìm các anti-patterns. Tạo record Finding(String file, int line, String rule, String snippet).

Tóm tắt

Nguyên tắcÁp dụng
Input ValidationWhitelist approach, validate ở mọi tầng
SQL InjectionLuôn dùng PreparedStatement, whitelist cho dynamic columns
SerializationTránh Java Serialization, dùng JSON, ObjectInputFilter
Error HandlingLog chi tiết cho dev, trả thông báo chung cho client
Least PrivilegeImmutable returns, typed parameters, limited DB permissions
Sensitive Datachar[] cho password, không log dữ liệu nhạy cảm

Tiếp theo

Ở bài tiếp theo, chúng ta sẽ tìm hiểu Lỗ hổng phổ biến & Phòng chống — OWASP Top 10 cho Java, deserialization attacks và dependency security.

Bài 6: Lỗ hổng phổ biến & Phòng chống