轉:mybatis是防止SQL注入原理

SQL注入是一種很簡單的攻擊手段,但直到今天仍然十分常見。究其原因不外乎:No patch for stupid。爲什麼這麼說,下面就以JAVA爲例進行說明:

假設數據庫中存在這樣的表:
[java] view plain copy
  1. table user(  
  2. id   varchar(20)    PRIMARY KEY ,         
  3. name     varchar(20)            ,  
  4. age  varchar(20)           );  
然後使用JDBC操作表:
[java] view plain copy
  1. private String getNameByUserId(String userId) {  
  2.     Connection conn = getConn();//獲得連接  
  3.     String sql = "select name from user where id=" + userId;  
  4.     PreparedStatement pstmt =  conn.prepareStatement(sql);  
  5.     ResultSet rs=pstmt.executeUpdate();  
  6.     ......  
  7. }  

上面的代碼經常被一些開發人員使用。想象這樣的情況,當傳入的userId參數爲"3;drop table user;"時,執行的sql語句如下:
[java] view plain copy
  1. select name from user where id=3; drop table user;  

數據庫在編譯執行之後,刪除了user表。瞧,一個簡單的SQL注入攻擊生效了!之所以這樣,是因爲上面的代碼沒有符合編程規範。

當我們按照規範編程時,SQL注入就不存在了。這也是避免SQL注入的第一種方式:預編譯語句,代碼如下:
[java] view plain copy
  1.    Connection conn = getConn();//獲得連接  
  2. String sql = "select name from user where id= ?";  
  3. PreparedStatement pstmt = conn.prepareStatement(sql);  
  4. pstmt.setString(1, userId);  
  5. ResultSet rs=pstmt.executeUpdate();  
  6. ......  

爲什麼上面的代碼就不存在SQL注入了呢?因爲使用了預編譯語句,預編譯語句在執行時會把"select name from user where id= ?"語句事先編譯好,這樣當執行時僅僅需要用傳入的參數替換掉?佔位符即可。而對於第一種不符合規範的情況,程序會先生成sql語句,然後帶着用戶傳入的內容去編譯,這恰恰是問題所在。

除了使用預編譯語句之外,還有第二種避免SQL注入攻擊的方式:存儲過程。存儲過程(Stored Procedure)是一組完成特定功能的SQL語句集,經編譯後存儲在數據庫中,用戶通過調用存儲過程並給定參數(如果該存儲過程帶有參數)就可以執行它,也可以避免SQL注入攻擊

[java] view plain copy
  1.    Connection conn = getConn();  
  2. stmt = conn.prepareCall("{call name_from_user(?,?)}");    
  3. stmt.setInt(1,2);    
  4. stmt.registerOutParameter(2, Types.VARCHAR);    
  5. stmt.execute();    
  6. String name= stmt.getString(2);   


上面的代碼中對應的存儲過程如下:
[java] view plain copy
  1. use user;  
  2. delimiter //  
  3. create  procedure name_from_user(in user_id int,out user_name varchar(20))  
  4. begin  
  5.     select name into user_name from user where id=user_id;  
  6. end  
  7. //  
  8. delimiter ;  


當然用戶也可以在前端做字符檢查,這也是一種避免SQL注入的方式:比如對於上面的userId參數,用戶檢查到包含分號就提示錯誤。
不過,從最根本的原因看,SQL注入攻擊之所以存在,是因爲app在訪問數據庫時沒有使用最小權限。想來也是,大家好像一直都在使用root賬號訪問數據庫。

那麼mybatis是如何避免sql注入攻擊的呢?還是以上面的表user爲例:
假設mapper文件爲:
[java] view plain copy
  1. <select id="getNameByUserId" resultType="String">  
  2.         SELECT name FROM user where id = #{userId}  
  3. </select>  


對應的java文件爲:
[java] view plain copy
  1. public interface UserMapper{  
  2.     String getNameByUserId(@Param("userId") String userId);  
  3. }  

可以看到輸入的參數是String類型的userId,當我們傳入userId="34;drop table user;"後,打印的語句是這樣的:
[java] view plain copy
  1. select name from user where id = ?  


不管輸入何種userID,他的sql語句都是這樣的。這就得益於mybatis在底層實現時使用預編譯語句。數據庫在執行該語句時,直接使用預編譯的語句,然後用傳入的userId替換佔位符?就去運行了。不存在先替換佔位符?再進行編譯的過程,因此SQL注入也就沒有了生存的餘地了。

那麼mybatis是如何做到sql預編譯的呢?其實框架底層使用的正是PreparedStatement類。PreparedStaement類不但能夠避免SQL注入,因爲已經預編譯,當N次執行同一條sql語句時,節約了(N-1)次的編譯時間,從而能夠提高效率。

如果將上面的語句改成:
[java] view plain copy
  1. <select id="getNameByUserId" resultType="String">  
  2.         SELECT name FROM user where id = ${userId}  
  3. </select>  
當我們輸入userId="34;drop table user;"後,打印的語句是這樣的:
[java] view plain copy
  1. select name from user where id = 34;drop table user;  


此時,mybatis沒有使用預編譯語句,它會先進行字符串拼接再執行編譯,這個過程正是SQL注入生效的過程。
因此在編寫mybatis的映射語句時,儘量採用“#{xxx}”這樣的格式。若不得不使用“${xxx}”這樣的參數,要手工地做好過濾工作,來防止sql注入攻擊。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章