前言
在java中,操作SQL的主要有以下幾種方式:
• java.sql.Statement
• java.sql.PrepareStatment
• 使用第三方ORM框架,MyBatis或者Hibernate
java.sql.Statement
java.sql.statement是最原始的執行SQL的接口,使用它需要手動拼接SQL語句。
String sql = "SELECT * FROM user WHERE id = '" + id + "'"; Statement statement = connection.createStatement(); statement.execute(sql);
這里很明顯就是存在問題的。將id直接拼接到SQL語句並執行,所以可以構造語句進行注入。
java.sql.PrepareStatement
這是一個接口是對上面的Statement的擴展。使用了預編譯。擁有防護SQL的特性
使用時,在SQL語句中,用 ? 作為占位符,代替需要傳入的參數,然后將該語句傳遞給數據庫,數據庫會對這條語句進行預編譯。如果要執行這條SQL,只要用特定的 set 方法,將傳入的參數設置到SQL語句中的指定位置,然后調用 execute 方法執行這條完整的SQL。示例如下:
String sql = "SELECT * FROM user WHERE id = ?"; //預編譯語句 PreparedStatement preparedStatement = connection.prepareStatement(sql); //填入參數 preparedStatement.setString(1,reqStuId); preparedStatement.executeQuery();
此時,如果我用之前的請求攻擊,執行的SQL會變成 SELECT * FROM user WHERE id = ''or 1 #',可以看到單引號是被轉義了,同時參數也被一對單引號包裹,數字型注入也不存在了
特殊情況ORDER BY注入
我們已經知道,通過占位符傳參,不管傳遞的是什么類型的值,都會被單引號包裹。而使用 ORDER BY 時,要求傳入的是字段名或者是字段位置,如:
String sql = "SELECT * FROM user ORDER BY " + column;
那么這樣依然可能會存在SQL注入的問題,在 Java 中會有兩種情況:
- column 是字符串型
這種情況和 Statement 中描述的一樣,是存在注入的。要防御就必須要手動過濾,或者將字段名硬編碼到 SQL 語句中,比如:
String column = "id"; String sql =""; switch(column){ case "id": sql = "SELECT * FROM user ORDER BY id"; break; case "username": sql = "SELECT * FROM user ORDER BY username"; break; ...... }
同樣group by也存在同樣的問題
MyBatis
MyBatis簡介
MyBatis的使用方式主要有倆種,一種是使用注解,直接將SQL語句和方法綁定在一起,如下
package org.mybatis.example; public interface BlogMapper { @Select("SELECT * FROM blog WHERE id = #{id}") Blog selectBlog(int id); }
這種方式,適合簡單的SQL語句,一旦語句長了,注釋會變得復雜混亂,維護起來很麻煩,所以它只適合小項目(小項目用的也不多)。
用的最多的是第二種——XML配置,將SQL語句和Java代碼分離,有獨立的xml文件,描述某個方法會和某個SQL語句綁定。
如圖,每一個接口,在資源文件目錄中,都有對應的xml。接口中的方法,和xml中id相同的SQL語句關聯。
例如,IArticleCateDao 的 list()方法被調用,那么就會找到 ArticleCateMapper.xml中 id等於 list 的方法,執行它的 SQL,然后根據 resultMap 描述的 字段-屬性 映射關系,返回相應的實例對象。
這里的 resultMap 具體如下:
其中,id屬性是該映射的名稱,type屬性代表映射的類。里面有 5 個子元素,id元素映射到ArticleCate的id屬性。其它四個result元素中的column屬性會映射到對應的property屬性。
MyBatis注入問題
${} (不安全的寫法)
使用 ${foo} 這樣格式的傳入參數會直接參與SQL編譯,類似字符串拼接的效果,是存在SQL注入漏洞的。所以一般情況下,不會用這種方式綁定參數。
{}(安全的寫法)
使用 #{} 做參數綁定時, MyBatis 會將SQL語句進行預編譯,避免SQL注入的問題。
MyBatis 預編譯模式的實現,在底層同樣是依賴於 java.sql.PreparedStatement,所以 PreparedStatement 存在的問題,這里也會存在。
ORDER BY 只能通過 ${} 傳遞。為了避免SQL注入,需要手動過濾,或者在SQL里硬編碼 ORDER BY 的字段名。
此外,還有一種情況 —— LIKE 模糊查詢。
看下面這個寫法:
在這里,MyBatis 會把 %#{stuName}% 作為要查詢的參數,數據庫會執行 SELECT * FROM student WHERE student.stu_name LIKE '%#{stuName}%',導致查詢失敗,經過查詢失敗的原因是這么寫經MyBatis轉換后(‘%#{name}%’)會變為(‘%?%’),而(‘%?%’)會被看作是一個字符串,所以Java代碼在執行找不到用於匹配參數的 ‘?’ ,然后就報錯了。
所以這里只能用 ${} 的方式傳入。而如果用 ${} 又存在SQL注入的風險,怎么辦呢?
最好的方法是,使用數據庫自帶的 CONCAT ,將 % 和我們用 #{} 傳入參數連接起來,這樣就既不存在注入的問題,也能滿足需求啦。示例:
還有一下幾種方式避免模糊查詢
把’%#{name}%’改為”%”#{name}”%” 使用標簽的方式
Hibernate注入問題
Hibernate 是一個高性能的 ORM 框架,可以自動生成 SQL 語句,通常與 Struts、Spring 一起搭配使用,也就是我們熟知的 SSH 框架。
HQL 是 Hibernate 獨有的面向對象的查詢語言,接近 SQL。Hibernate引擎會對 HQL 進行解析,翻譯成 SQL,再將 SQL 交給數據庫執行。
HQL 會出現注入的地方還是在字符串拼接的時候,審計的時候看看 SQL 是不是用加號 + 的就行了。
session.createQuery("from Book where title like '%" + userInput + "%' and published = true")
下面來看看 HQL 能防注入的安全寫法。
第一種,使用具名參數 Named parameter:
List
第二種,占位符 Positional parameter:
List