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


您可在下列課程中了解更多技巧喔!