Apache Derby Bài 17: Phòng chống SQL Injection bằng PreparedStatement
1. Cơn ác mộng mang tên SQL Injection
Trong Bài 16, chúng ta đã dùng Statement kết hợp với toán tử nối chuỗi (+) của Java để tạo ra câu lệnh SQL. Tuy nhiên, nếu áp dụng cách này cho dữ liệu nhập vào từ người dùng (như form đăng nhập, ô tìm kiếm), bạn đang mở toang cánh cửa cho lỗ hổng bảo mật khét tiếng: SQL Injection.
Hãy xem xét kịch bản truy vấn kiểm tra đăng nhập bằng Statement:
Java
String userInput = "admin' OR '1'='1"; // Kẻ tấn công nhập chuỗi này vào ô Username
String sql = "SELECT * FROM tai_khoan WHERE username = '" + userInput + "'";
Sau khi nối chuỗi, Derby sẽ nhận được câu lệnh:
SQL
SELECT * FROM tai_khoan WHERE username = 'admin' OR '1'='1'
Vì điều kiện '1'='1' luôn đúng, kẻ tấn công có thể dễ dàng vượt qua màn hình đăng nhập mà không cần biết mật khẩu, hoặc nguy hiểm hơn là chèn thêm lệnh ; DROP TABLE tai_khoan; để phá hủy hệ thống.
2. Giải pháp: PreparedStatement và Tham số hóa (Parameterized Queries)
Để giải quyết triệt để vấn đề này, JDBC cung cấp interface PreparedStatement (kế thừa từ Statement).
Thay vì nối chuỗi dữ liệu thô vào câu lệnh SQL, PreparedStatement sử dụng dấu chấm hỏi (?) làm các tham số giữ chỗ (placeholders).
Java
// Khai báo sẵn bộ khung SQL với các dấu ?
String sql = "SELECT * FROM tai_khoan WHERE username = ? AND password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
Cơ chế bảo mật hoạt động ra sao?
-
Biên dịch trước (Pre-compilation): Khi bạn gọi
conn.prepareStatement(sql), JDBC gửi ngay "bộ khung" SQL đó (chỉ chứa các dấu?) đến Derby Engine. Derby sẽ phân tích cú pháp và chốt chặt cấu trúc logic của câu lệnh này. -
Ép kiểu dữ liệu nghiêm ngặt: Khi bạn gán giá trị cho dấu
?, Driver sẽ tự động xử lý các ký tự đặc biệt (thoát các dấu nháy đơn, ngoặc kép). Dữ liệu truyền vào được đối xử hoàn toàn là giá trị thuần túy (literal value), tuyệt đối không bao giờ được hiểu là một phần của mã lệnh SQL (executable code).
3. Lợi ích kép: Hiệu năng tối ưu từ Derby Engine
Bên cạnh bảo mật, PreparedStatement còn là chìa khóa để đạt hiệu năng cao trong Apache Derby.
Trong Derby, quá trình biên dịch một câu lệnh SQL (phân tích từ vựng, kiểm tra quyền, lập kế hoạch thực thi - execution plan) tốn nhiều chi phí CPU hơn là chạy nó.
-
Khi dùng
Statementnối chuỗi,INSERT INTO bang VALUES (1)vàINSERT INTO bang VALUES (2)được Derby coi là hai câu lệnh hoàn toàn khác nhau, phải biên dịch lại từ đầu. -
Khi dùng
PreparedStatementvớiINSERT INTO bang VALUES (?), Derby chỉ biên dịch và tạo kế hoạch thực thi đúng một lần, lưu vào bộ nhớ đệm (Statement Cache). Ở các lần gọi tiếp theo (dù tham số có thay đổi), nó chỉ việc thay thế giá trị và thực thi ngay lập tức, giúp tăng tốc độ đáng kể cho các tác vụ xử lý hàng loạt (Batch Processing).
4. Thực hành: Cú pháp chuẩn với PreparedStatement
Quy trình sử dụng PreparedStatement:
-
Tạo bộ khung SQL với các dấu
?. -
Khởi tạo đối tượng qua
conn.prepareStatement(sql). -
Gọi các hàm
setXxx(index, value)để điền dữ liệu (chỉ sốindexbắt đầu từ 1). -
Gọi
executeUpdate()(choINSERT/UPDATE/DELETE) hoặcexecuteQuery()(choSELECT) – không truyền lại chuỗi SQL vào các hàm này.
Java
package com.hieu.derby;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DerbyPreparedStatementDemo {
private static final String JDBC_URL = "jdbc:derby:my_embedded_db";
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection(JDBC_URL)) {
// 1. CHÈN DỮ LIỆU AN TOÀN
String insertSql = "INSERT INTO nhan_vien (id, ho_ten, luong) VALUES (?, ?, ?)";
try (PreparedStatement insertStmt = conn.prepareStatement(insertSql)) {
// Điền tham số cho bản ghi 1
insertStmt.setInt(1, 3);
insertStmt.setString(2, "Le Van C");
insertStmt.setDouble(3, 15000000.00);
insertStmt.executeUpdate(); // Chạy lệnh
// Điền tham số cho bản ghi 2 (Tái sử dụng lại PreparedStatement)
insertStmt.setInt(1, 4);
insertStmt.setString(2, "O'Brian"); // Chứa dấu nháy đơn, sẽ được xử lý an toàn
insertStmt.setDouble(3, 22000000.00);
insertStmt.executeUpdate(); // Chạy lệnh
System.out.println("Đã chèn dữ liệu thành công!");
}
// 2. TRUY VẤN DỮ LIỆU AN TOÀN
String selectSql = "SELECT id, ho_ten, luong FROM nhan_vien WHERE luong >= ?";
try (PreparedStatement selectStmt = conn.prepareStatement(selectSql)) {
// Lọc những người có lương >= 20 triệu
selectStmt.setDouble(1, 20000000.00);
try (ResultSet rs = selectStmt.executeQuery()) {
System.out.println("--- Danh sách nhân viên lương cao ---");
while (rs.next()) {
System.out.println("ID: " + rs.getInt("id") +
" | Tên: " + rs.getString("ho_ten") +
" | Lương: " + rs.getDouble("luong"));
}
}
}
} catch (SQLException e) {
System.err.println("Lỗi cơ sở dữ liệu: " + e.getMessage());
}
}
}
5. Tổng kết
-
Luôn luôn sử dụng
PreparedStatementcho bất kỳ câu lệnh SQL nào có chứa biến số hoặc dữ liệu ngoại lai đầu vào.Statementchỉ nên dùng cho câu lệnh tĩnh (DDL nhưCREATE TABLE). -
Các hàm
setXxx(index, value)đếm từ 1 và phải phù hợp với kiểu dữ liệu của cột trong cơ sở dữ liệu. -
PreparedStatementbảo vệ hệ thống khỏi SQL Injection bằng cách tách biệt hoàn toàn giữa cấu trúc lệnh SQL và dữ liệu thực thi, đồng thời tận dụng bộ đệm biên dịch (Statement Cache) của Derby Engine để tối ưu hóa hiệu suất.
Bài tiếp theo sẽ đề cập đến một quy trình quan trọng thường bị bỏ quên trong chế độ Embedded: Apache Derby Bài 18: Quản lý vòng đời cơ sở dữ liệu nhúng và cơ chế tắt database (shutdown=true).
All rights reserved