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隱碼攻擊了。