整理了一些Java方面的架構、面試資料(微服務、集羣、分佈式、中間件等),有須要的小夥伴能夠關注公衆號【程序員內點事】,無套路自行領取javascript
更多優選php
MyBatis
是一種持久層框架,介於 JDBC
和 Hibernate
之間。經過 MyBatis 減小了手寫 SQL 語句的痛苦,使用者能夠靈活使用 SQL 語句,支持高級映射。可是 MyBatis 的推出不是隻是爲了安全問題,有不少開發認爲使用了 MyBatis 就不會存在 SQL 注入了,真的是這樣嗎?java
使用了 MyBatis 就不會有 SQL 注入了嗎? 答案很明顯是 NO。 MyBatis它只是一種持久層框架,它並不會爲你解決安全問題。固然,若是你可以遵循規範,按照框架推薦的方法開發,天然也就避免 SQL 注入問題了。本文就將 MyBatis 和 SQL 注入這些恩恩怨怨掰扯掰扯。(注本文所說的 MyBatis 默認指的是 Mybatis3)mysql
寫本文的起源主要是來源於內網發現的一次 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 注入的問題。數據庫
分析一下這個開發爲何會這麼寫,在他沒有意識到這樣的寫法存在 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 注入問題targetRuntime
的配置,若是不須要複雜的條件查詢的話,建議直接使用 MyBatis3Simple
。這樣能夠更好地直接杜絕風險,由於一旦有風險點,就有發生問題的可能。做者:madneal@平安銀行應用安全團隊 ,查看原文
今天就說這麼多,若是本文對您有一點幫助,但願能獲得您一個點贊👍哦
您的承認纔是我寫做的動力!
整理了一些Java方面的架構、面試資料(微服務、集羣、分佈式、中間件等),有須要的小夥伴能夠關注公衆號【程序員內點事】,無套路自行領取