引言
MyBatis
是一種持久層架構,介于
JDBC
和
Hibernate
之間。通過 MyBatis 減少了手寫 SQL 語句的痛苦,使用者可以靈活使用 SQL 語句,支援進階映射。但是 MyBatis 的推出不是隻是為了安全問題,有很多開發認為使用了 MyBatis 就不會存在 SQL 注入了,真的是這樣嗎?
使用了 MyBatis 就不會有 SQL 注入了嗎? 答案很明顯是 NO。 MyBatis它隻是一種持久層架構,它并不會為你解決安全問題。當然,如果你能夠遵循規範,按照架構推薦的方法開發,自然也就避免 SQL 注入問題了。本文就将 MyBatis 和 SQL 注入這些恩恩怨怨掰扯掰扯。(注本文所說的 MyBatis 預設指的是 Mybatis3)
技術背景
寫本文的起源主要是來源于内網發現的一次 SQL 注入。我們發現内網的一個請求的
keyword
參數存在 SQL 注入,簡單地介紹一下需求背景。
基本上這個接口就是實作多個字段可以實作 keyword 的模糊查詢,這應該是一個比較常見的需求。隻不過這裡存在多個查詢條件。經過一番搜尋,我們發現問題的核心處于以下代碼:
public Criteria addKeywordTo(String keyword) {
StringBuilder sb = new StringBuilder();
sb.append("(display_name like '%" + keyword + "%' or ");
sb.append("org like '" + keyword + "%' or ");
sb.append("status like '%" + keyword + "%' or ");
sb.append("id like '" + keyword + "%') ");
addCriterion(sb.toString());
return (Criteria) this;
}
複制
很明顯,需求是希望實作
diaplay_name
,
org
,
status
以及
id
的模糊查詢,但開發在這裡自己建立了一個
addKeywordTo
方法,通過這個方法建立了一個涉及多個字段的模糊查詢條件。
有一個有趣的現象,在内網發現的絕大多數 SQL 注入的注入點,基本都是
模糊查詢
的地方。可能很多開發往往覺得模糊查詢是不是就不會存在 SQL 注入的問題。
分析一下這個開發為什麼會這麼寫,在他沒有意識到這樣的寫法存在 SQL 注入問題的時候,這樣的寫法他可能認為是最省事的,到時直接把查詢條件拼進去就可以了。以上代碼是問題的核心,我們再看一下對應的 xml 檔案:
<sql id="Example_Where_Clause" >
<where >
<foreach collection="oredCriteria" item="criteria" separator="or" >
<if test="criteria.valid" >
<trim prefix="(" suffix=")" prefixOverrides="and" >
<foreach collection="criteria.criteria" item="criterion" >
<choose >
<when test="criterion.noValue" >
and ${criterion.condition}
</when>
<when test="criterion.singleValue" >
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue" >
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue" >
and ${criterion.condition}
<foreach collection="criterion.value" item="listItem" open="(" close=")" separator="," >
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
複制
<select id="selectByExample" resultMap="BaseResultMap" parameterType="com.doctor.mybatisdemo.domain.userExample" >
select
<if test="distinct" >
distinct
</if>
<include refid="Base_Column_List" />
from user
<if test="_parameter != null" >
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null" >
order by ${orderByClause}
</if>
</select>
複制
我們再回過頭看一下上面
JAVA
代碼中的
addCriterion
方法,這個方法是通過
MyBatis generator
生成的。
protected void addCriterion(String condition) {
if (condition == null) {
throw new RuntimeException("Value for condition cannot be null");
}
criteria.add(new Criterion(condition));
}
複制
這裡的
addCriterion
方法隻傳入了一個字元串參數,這裡其實使用了重載,還有其它的
addCriterion
方法傳入的參數個數不同。這裡使用的方法隻傳入了一個參數,被了解為
condition
,是以隻是添加了一個隻有
condition
的
Criterion
。現在再來看 xml 中的
Example_Where_Clause
,在周遊
criteria
時,由于 criterion 隻有 condition 沒有 value,那麼隻會進去條件
criterion.noValue
,這樣整個 SQL 注入的形成就很清晰了。
<when test="criterion.noValue" >
and ${criterion.condition}
</when>
複制
正确寫法
既然上面的寫法不正确,那正确的寫法應該是什麼呢?
第一種,我們可以用一種非常簡單直接的方法,在
addKeywordTo
方法裡面 對
keword
進行過濾,這樣其實也可以避免 SQL 注入。通過正則比對将
keyword
裡面所有非字母或者數字的字元都替換成空字元串,這樣自然也就不可能存在 SQL 注入了。
keyword = keyword.replaceAll("[^a-zA-Z0-9\s+]", "");
複制
但是這種寫法并不是一種科學的寫法,這樣的寫法存在一種弊端,就是如果你的
keyword
需要包含符号該怎麼辦,那麼你是不是就要考慮更多的情況,是不是就需要添加更多的邏輯判斷,是不是就存在被繞過的可能了?那麼正确的寫法應該是什麼呢?其實
mybatis 官網
已經給出了
Comple Queries
的範例:
TestTableExample example = new TestTableExample();
example.or()
.andField1EqualTo(5)
.andField2IsNull();
example.or()
.andField3NotEqualTo(9)
.andField4IsNotNull();
List<Integer> field5Values = new ArrayList<Integer>();
field5Values.add(8);
field5Values.add(11);
field5Values.add(14);
field5Values.add(22);
example.or()
.andField5In(field5Values);
example.or()
.andField6Between(3, 7);
複制
上面等同的 SQL 語句是:
where (field1 = 5 and field2 is null)
or (field3 <> 9 and field4 is not null)
or (field5 in (8, 11, 14, 22))
or (field6 between 3 and 7)
複制
現在讓我們将一開始的
addKeywordTo
方法進行改造:
public void addKeywordTo(String keyword, UserExample userExample) {
userExample.or().andDisplayNameLike("%" + keyword + "%");
userExample.or().andOrgLike(keyword + "%");
userExample.or().andStatusLike("%" + keyword + "%");
userExample.or().andIdLike(keyword + "%");
}
複制
這樣的寫法才是一種比較标準的寫法了。
or()
方法會産生一個新的
Criteria
對象,添加到
oredCriteria
中,并傳回這個
Criteria
對象,進而可以鍊式表達,為其添加
Criterion
。這樣添加的的
Criteria
就是包含
condition
以及
value
的,在做條件查詢的時候,就會進入到
criterion.singleValue
中,那麼 keyword 參數隻會傳入到
value
中,而
value
是通過
#{}
傳入的。
<when test="criterion.singleValue" >
and ${criterion.condition} #{criterion.value}
</when>
複制
總結一下,導緻這個 SQL 注入的原因還是開發沒有按照規範來寫,自己造輪子寫了一個方法來進行模糊查詢,殊不知帶來了 SQL 注入漏洞。其實,
Mybatis generator
已經為每個字段生成了豐富的方法,隻要合理使用,就一定可以避免 SQL 注入問題。

在這裡插入圖檔描述
使用 #{} 可以避免 SQL 注入嗎?
如果你猛地一看到這個問題,你可能會覺得遲疑?使用
#{}
就可以徹底杜絕 SQL 注入麼,不一定吧。但如果你仔細分析一下,你就會發現答案是肯定的。具體的原因讓我和你娓娓道來。
首先我們需要先搞清楚 MyBatis 中
#{}
是如何聲明的。當參數通過
#{}
聲明的,參數就會通過
PreparedStatement
來執行,即預編譯的方式來執行。預編譯你應該不陌生,因為在
JDBC
中就已經有了預編譯的接口。
這也對應了開頭文中我們提到的一點,Mybatis 并不是能解決 SQL 注入的核心,預編譯才是。預編譯不僅可以對 SQL 語句進行轉義,避免 SQL 注入,還可以增加執行效率。Mybatis 底層其實也是通過 JDBC 來實作的。以 MyBatis 3.3.1 為例,jdbc 中的 SqlRunner 就設計到具體 SQL 語句的實作。
在這裡插入圖檔描述
以 update 方法為例,可以看到就是通過 JAVA 中
PreparedStatement
來實作 sql 語句的預編譯。
public int update(String sql, Object... args) throws SQLException {
PreparedStatement ps = this.connection.prepareStatement(sql);
int var4;
try {
this.setParameters(ps, args);
var4 = ps.executeUpdate();
} finally {
try {
ps.close();
} catch (SQLException var11) {
;
}
}
return var4;
}
複制
值得注意的一點是,這裡的
PreparedStatement
嚴格意義上來說并不是完全等同于預編譯。其實預編譯分為用戶端的預編譯以及服務端的預編譯,4.1 之後的 MySql 伺服器端已經支援了預編譯功能。
很多主流
持久層架構
(
MyBatis
,
Hibernate
) 其實都沒有真正的用上預編譯,預編譯是要我們自己在參數清單上面配置的,如果我們不手動開啟,JDBC 驅動程式 5.0.5 以後版本 預設預編譯都是關閉的。
需要通過配置參數來進行開啟:
jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true&cachePrepStmts=true
複制
資料庫 SQL 執行包含多個階段如下圖所示,但我們這裡針對于 SQL 語句用戶端的預編譯在發送到服務端之前就已經完成了。在伺服器端主要考慮的就是性能問題,這不是本文的重點。
當然,每一個資料庫實作的預編譯方式可能都有一些差别。但是對于防止 SQL 注入,在 MyBatis 中隻要使用
#{}
就可以了,因為這樣就會實作 SQL 語句的參數化,避免直接引入惡意的 SQL 語句并執行。
在這裡插入圖檔描述
MyBatis generator 的使用
對于使用
MyBatis
,
MyBatis generator
肯定是必不可少的使用工具。MyBatis 是針對 MyBatis 以及 iBATIS 的代碼生成工具,支援 MyBatis 的所有版本以及 iBATIS 2.2.0 版本以上。
因為在現實的業務開發中,肯定會涉及到很多表,開發不可能自己一個去手寫相應的檔案。通過 MyBatis generator 就可以生成相應的
POJO 檔案
、
SQL Map XML
檔案以及可選的 JAVA 用戶端代碼。
常用的使用 MyBatis generator 的方式是直接通過使用 Maven 的
mybatis-generator-maven-plugin
插件,隻要準備好配置檔案以及資料庫相關資訊,就可以通過這個插件生成相應代碼了。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="MysqlTables" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressAllComments" value="false" />
<property name="suppressDate" value="false" />
</commentGenerator>
<!-- 資料庫連結URL、使用者名、密碼 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybaits_test"
userId="xxx"
password="xxx">
</jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="true" />
</javaTypeResolver>
<javaModelGenerator targetPackage="com.doctor.mybatisdemo.domain" targetProject="src/main/java/">
<property name="constructorBased" value="false" />
<property name="enableSubPackages" value="false" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<sqlMapGenerator targetPackage="myBatisGeneratorDemoConfig" targetProject="src/main/resources">
<property name="enableSubPackages" value="false" />
</sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.doctor.mybatisdemo.dao" targetProject="src/main/java/">
<property name="enableSubPackages" value="false" />
</javaClientGenerator>
<!-- 要生成那些表(更改tableName和domainObjectName就可以) -->
<table tableName="user" domainObjectName="user"/>
</context>
</generatorConfiguration>
複制
在這裡插入圖檔描述
在這裡我想強調的是一個關鍵參數的配置,即
targetRuntime
參數。這個參數有2種配置項,即
MyBatis3
和
MyBatis3Simple
,MyBatis3 為預設配置項。
MyBatis3Simple
隻會生成基本的增删改查,而
MyBatis3
會生成帶條件的增删改查,所有的條件都在 XXXexample 中封裝。
使用 MyBatis3 時,
enableSelectByExample
,
enableDeleteByExample
,
enableCountByExample
以及
enableUpdateByExample
這些屬性為 true,就會生成相應的動态語句。這也就是我們上述 Example_Where_Clause 生成的原因。
如果使用配置項 MyBatis3Simple,那麼生成的 SQL Map XML 檔案将非常簡單,隻包含一些基本的方法,也不會産生上面的動态方法。可以這麼說,如果你使用 MyBatis3Simple 話,并且不額外改造,因為裡面所有的變量都是通過
#{}
引入,就不可能會有 SQL 注入的問題。
但是現實業務中往往涉及到複雜的查詢條件,而且一般開發使用的都是祖傳配置檔案,是以到底是使用 MyBatis3 還是 MyBatis3Simple,還是需要具體問題,具體看待。不過如果你是使用預設配置,你就需要當心了,謹記一點,外部傳入的參數是極有可能是不安全的,是不可以直接引入處理的。意思到這一點,就基本可以很好地避免 SQL 注入問題了。
總結
這篇文章從内網的一個 SQL 注入漏洞引發的對 MyBatis 的使用問題思考,對 MyBatis 中
#{}
工作的原理以及
Mybatis generator
的使用多個方面做了進一步的思考。
可以總結以下幾點:
- 能不使用拼接就不要使用拼接,這應該也是避免
最基本的原則SQL 注入
- 在使用
傳入變量的時候,一定要注意變量的引入和過濾,避免直接通過 ${} 傳入外部變量${}
- 不要自己
,尤其是在安全方面,其實在這個問題上,架構已經提供了标準的方法。如果按照規範開發的話,也不會導緻 SQL 注入問題造輪子
- 可以注意 MyBatis 中
的配置,如果不需要複雜的條件查詢的話,建議直接使用targetRuntime
。這樣可以更好地直接杜絕風險,因為一旦有風險點,就有發生問題的可能。MyBatis3Simple
作者:madneal@平安銀行應用安全團隊 ,檢視原文
今天就說這麼多,如果本文對您有一點幫助,希望能得到您一個點贊👍哦
您的認可才是我寫作的動力!
整理了一些Java方面的架構、面試資料(微服務、叢集、分布式、中間件等),有需要的小夥伴可以關注公衆号【程式員内點事】,無套路自行領取