JDBC如何防範SQL隱碼攻擊
(SQL Injection)
戴玉佩 Patty Tai
- 精誠資訊/恆逸教育訓練中心-資深講師
- 技術分類:程式設計
開發應用系統時不可避免都要存取資料庫中的資料,尤其在Web應用系統,當資料是由廣域網路不知名的使用者輸入時,如何防範SQL隱碼攻擊 (SQL Injection) 也成為各家程式語言資料庫解決方案中必要的重點開發技巧。
所謂「SQL隱碼攻擊」,簡單來說,就是使用者在輸入欄位資料時,惡意地在資料中夾雜一些SQL指令,當這些資料送達後端併入完整指令中,就可能對原有指令的結構產生特殊的效果。接著由JDBC將整段指令一起送達資料庫處理時,就讓資料庫服務發生非預期的資訊外洩或破壞了table的內容。下列示範一個最常見的例子。
首先筆者在MySQL伺服器建立了測試用的資料庫,且在其中建立users資料表格,並新增了兩筆測試用的使用者資料:
先示範一個無法防止SQL隱碼攻擊的測試程式如下:
package uuu.tips.test;
import java.sql.*;
import java.util.Scanner;
import java.util.logging.*;
public class TestUserLogin {
private static final Logger tipsLogger = Logger.getLogger("UCOM Tips文章");
public static void main(String[] args) {
String id, password;
Scanner scanner = new Scanner(System.in);
System.out.println("請輸入帳號:"); //John
id = scanner.next();
System.out.println("請輸入密碼:"); //asdf1234
password = scanner.next();
final String sql = "SELECT id, password, name, gender, birthday, email "
+ "FROM users WHERE id='"
+id+"' AND password='"+password+"'";
//資料庫元件皆使用try-with-resource語法確保在程式結束時會正常close
try ( Connection connection = RDBConnection.getConnection();//2.建立連線
Statement stmt = connection.createStatement();//3.建立指令
ResultSet rs = stmt.executeQuery(sql); //4.執行指令
){
//5.處理rs
int i = 0;
while(rs.next()) {
System.out.println("登入"+ ++i +"位成功");
System.out.print("帳號: " + rs.getString("id"));
System.out.print("姓名: " + rs.getString("name"));
System.out.print("性別: " + rs.getString("gender"));
System.out.print("生日: " + rs.getString("birthday"));
System.out.println("Email: " + rs.getString("email"));
}
if(i<1)System.out.println("登入失敗,帳號或密碼不正確");
} catch (SQLException e) {
tipsLogger.log(Level.SEVERE, "登入失敗", e);
} catch (Exception e) {
tipsLogger.log(Level.SEVERE, "登入功能發生錯誤", e);
}
}
}
第一次測試時使用正常的測試資料(帳號輸入John,密碼則輸入asdf1234)。結果如下:
第二次測試使用正常但錯誤的資料(帳號輸入john,密碼則輸入aaaa1234)。結果如下:
這次測試時使用特殊的資料(帳號輸入'OR'1'='1,密碼則輸入'OR'1'='1)。這看起來一定會發生登入失敗的帳號密碼,卻得到如下的結果:
因為當程式把這樣的輸入資料加入原來的SQL指令時,就讓整個WHERE子句變成一個恆真式,所以全部的使用者資料都被查出來了:
要避免這樣的攻擊方式,只要將SQL指令與使用者輸入的資料藉由PreparedStatement來分開處理即可。程式改寫如下:
package uuu.mod19.test;
import java.sql.*;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
public class TestCustomerLogin2 {
private static final Logger tipsLogger = Logger.getLogger("UCOM Tips文章");
public static void main(String[] args) {
String id, password;
Scanner scanner = new Scanner(System.in);
System.out.println("請輸入帳號:"); //John
id = scanner.next();
System.out.println("請輸入密碼:"); //asdf1234
password = scanner.next();
final String sql = "SELECT id, password, name, gender, birthday, email "
+ "FROM users WHERE id=? AND password=?";
try ( Connection connection = RDBConnection.getConnection(); //2.建立連線
PreparedStatement pstmt =
connection.prepareStatement(sql); //3.準備指令
){
//執行前才傳入?對應的輸入值
pstmt.setString(1, id);
pstmt.setString(2, password);
//4.執行指令並取回ResultSet rs。使用try-with-resource語法確保rs.close()
try(ResultSet rs = pstmt.executeQuery();){
//5. 處理rs
int i = 0;
while(rs.next()) {
System.out.println("登入"+ ++i +"位成功");
System.out.print("帳號: " + rs.getString("id"));
System.out.print("姓名: " + rs.getString("name"));
System.out.print("性別: " + rs.getString("gender"));
System.out.print("生日: " + rs.getString("birthday"));
System.out.println("Email: " + rs.getString("email"));
}
if(i<1)System.out.println("登入失敗,帳號或密碼不正確");
}
} catch (SQLException e) {
tipsLogger.log(Level.SEVERE, "登入失敗", e);
} catch (Exception e) {
tipsLogger.log(Level.SEVERE, "登入功能發生錯誤", e);
}
}
}
第一次測試時使用正常的測試資料(帳號輸入John,密碼則輸入asdf1234)。結果如下:
第二次測試使用正常但錯誤的資料(帳號輸入john,密碼則輸入aaaa1234)。結果如下:
這次也用特殊資料(帳號輸入'OR'1'='1,密碼則輸入'OR'1'='1)。在PreparedStatement與SQL字串中用「?」暫時替代使用者輸入的資料,在執行前才傳入「?」的值,得到如下正確的結果:
如此一來,就可以確實防範SQL隱碼攻擊了。