Mybatis-plus?sql注入及防止sql注入詳解
一、SQL注入是什么?
SQL注入是一種代碼注入技術(shù),用于攻擊數(shù)據(jù)驅(qū)動(dòng)的應(yīng)用,惡意的SQL語(yǔ)句被 插入到執(zhí)行的SQL語(yǔ)句中來(lái)改變查詢結(jié)果,例如: OR 1=1 或者 ;drop table sys_user;等等
二、mybatis是如何做到防止sql注入的
mybatis中我們所寫(xiě)的sql語(yǔ)句都是在xml只能完成,我們?cè)诰帉?xiě)sql會(huì)用到 #{},${} 這個(gè)兩個(gè)表達(dá)式。那 #{} 和 ${}兩者之間有什么區(qū)別嘞?下面我將用兩個(gè)SQL語(yǔ)句例子來(lái)進(jìn)行說(shuō)明。
<select id="selectUserByUserName" parameterType="java.lang.String" resultType="com.domain.UserInfo"> SELECT USER_ID, USER_NAME, PWD, USER_PHONE FROM SYS_USER <where> USER_ID= #{userName,jdbcType=VARCHAR} </where> </select>
<select id="selectUserByUserName" parameterType="java.lang.String" resultType="com.domain.UserInfo"> SELECT USER_ID, USER_NAME, PWD, USER_PHONE FROM SYS_USER <where> USER_NAME= ${userName,jdbcType=VARCHAR} </where> </select>
- 第一種SQL語(yǔ)句中使用的#{}方式,#{}中當(dāng)傳入的數(shù)據(jù)是字符串,會(huì)在使用" "雙引號(hào)將值引起來(lái)。
- 示例:例如 userName 傳入的值是 9;DROP TABLE SYS_USER;那么#{}去取后得到的結(jié)果就是 USER_NAME="9;DROP TABLE SYS_USER;"就算傳入刪除表的命令也不會(huì)被執(zhí)行,因?yàn)?;DROP TABLE SYS_USER;會(huì)幫當(dāng)成一個(gè)完成的字符串去進(jìn)行值匹配。
- 第二種SQL${}方式取值,那就變成了USER_NAME=9;DROP TABLE SYS_USER; , 因 為 ${}直接將值拼接在SQL語(yǔ)句后面的,使其成為SQL,因此直接將值拼接在SQL語(yǔ)句后面的,因此${}是存在SQL注入的風(fēng)險(xiǎn)的,在使用時(shí)要注意手動(dòng)處理。
1. #{} 和 ${} 兩者的區(qū)別
- #{}:解析為一個(gè) JDBC 預(yù)編譯語(yǔ)句,一個(gè) #{} 被解析為一個(gè)參數(shù)占位符 ? ,#{}方式將傳入的數(shù)據(jù)都當(dāng)成一個(gè)字符串,會(huì)對(duì)自動(dòng)傳入的數(shù)據(jù)加一個(gè)雙引號(hào)。 如:WHERE USER_NAME =#{username},如果傳入的值是9,那么解析成sql時(shí)的值為WHERE USER_NAME =“9”,如果傳入的值是12345678,則解析成的sql為WHERE USER_NAME =“12345678”,
- ${} 僅 僅 為 一 個(gè) 純 粹 的 s t r i n g 替 換 ,${}方式傳入的變量直接拼接在sql中。如:WHERE USER_NAME = ${username},如果傳入的值是9,那么解析成sql時(shí)的值為WHERE USER_NAME =9; 如果傳入的值是;DROP TABLE SYS_USER;,則解析成的sql為:SELECT USER_ID, USER_NAME, PWD, USER_PHONE FROM SYS_USER WHERE USER_NAME="9;DROP TABLE SYS_USER;所以象 ORDER BY 或者 GROUP BY 等可以使用 ${}方式。
- #{}方式底層采用預(yù)編譯方式PreparedStatement,能夠很大程度防止sql注入,因?yàn)镾QL注入發(fā)生在編譯時(shí);${}方式底層只是Statement,無(wú)法防止Sql注入。
$方式一般用于傳入數(shù)據(jù)庫(kù)對(duì)象,例如傳入表名
2.PreparedStatement和Statement的區(qū)別
① PreparedStatement 在執(zhí)行sql命令時(shí),命令會(huì)先被數(shù)據(jù)庫(kù)進(jìn)行解析和編譯,然后將其放到命令緩存區(qū),然后,當(dāng)每一個(gè)執(zhí)行的相同的sql 命令時(shí),若在緩存區(qū)發(fā)了編譯命令,就不會(huì)再次進(jìn)行解析和編譯,這樣就可以進(jìn)行重復(fù)使用。PreparedStatement 在編譯是會(huì)將每個(gè)#{}標(biāo)記符號(hào)解析為參數(shù)參數(shù)占位符?,傳入的變量就是做為參數(shù),不會(huì)對(duì)sql語(yǔ)句進(jìn)行修改,這樣就能防止SQL注入的攻擊。‘’
②Statement是直接將Sql命令直接交給數(shù)據(jù)庫(kù)進(jìn)行運(yùn)行,不能做到攔截SQL注入的攻擊,因?yàn)镾QL注入時(shí)發(fā)生在運(yùn)行時(shí)。Statement每次都會(huì)對(duì)SQL命令進(jìn)行解析和編譯,增加大數(shù)據(jù)庫(kù)的開(kāi)銷,因此它效率不如PreparedStatement。
3.什么是預(yù)編譯
預(yù)編譯是做些代碼文本的替換工作。是整個(gè)編譯過(guò)程的最先做的工作。處理以# 開(kāi)頭的指令 , 比如拷貝 #include 包含的文件代碼,#define 宏定義的替換 , 條件編譯等,就是為編譯做的預(yù)備工作的階段。主要處理#開(kāi)始的預(yù)編譯指令,預(yù)編譯指令指示了在程序正式編譯前就由編譯器進(jìn)行的操作,可以放在程序中的任何位置。而SQL注入只能發(fā)生在運(yùn)行時(shí)。
4.mybaits-plus sql注入產(chǎn)生的原因
Mybatisplus中的 PaginationInterceptor 主要用于處理數(shù)據(jù)庫(kù)的物理分頁(yè),避免內(nèi)存分頁(yè)。
分析PaginationInterceptor 的源碼可以發(fā)現(xiàn)
Orderby場(chǎng)景下的SQL注入
前面提到了分頁(yè)中會(huì)存在Orderby的使用,因?yàn)镺rderby動(dòng)態(tài)查詢沒(méi)辦法進(jìn)行預(yù)編譯,所以不經(jīng)過(guò)安全檢查的話會(huì)存在注入風(fēng)險(xiǎn)。PaginationInnerInterceptor主要是通過(guò)設(shè)置com.baomidou.mybatisplus.extension.plugins.pagination.page對(duì)象里的屬性來(lái)實(shí)現(xiàn)orderby的,主要是以下函數(shù)的調(diào)用,因?yàn)橹苯邮褂胹ql拼接,所以需要對(duì)進(jìn)行排序的列名進(jìn)行安全檢查:
page.setAscs(); page.setDescs();
源碼:
可以看出,分頁(yè)是通過(guò)字符串拼接的方式,所以出現(xiàn)SQL注入的風(fēng)險(xiǎn)
public static String concatOrderBy(String originalSql, IPage<?> page, boolean orderBy) { if (!orderBy || !ArrayUtils.isNotEmpty(page.ascs()) && !ArrayUtils.isNotEmpty(page.descs())) { return originalSql; } else { StringBuilder buildSql = new StringBuilder(originalSql); String ascStr = concatOrderBuilder(page.ascs(), " ASC"); String descStr = concatOrderBuilder(page.descs(), " DESC"); if (StringUtils.isNotEmpty(ascStr) && StringUtils.isNotEmpty(descStr)) { ascStr = ascStr + ", "; } if (StringUtils.isNotEmpty(ascStr) || StringUtils.isNotEmpty(descStr)) { buildSql.append(" ORDER BY ").append(ascStr).append(descStr); } return buildSql.toString(); } }
三、Mybatis-plus是如何做到防止sql注入的
在使用分頁(yè)的controller,對(duì)傳入的分頁(yè)插件,對(duì)ascs與descs進(jìn)行檢查,判斷是否有非法字符,如有,則提示參數(shù)中含有非法的列名:create_time aaaa
示例:
校驗(yàn)字段的util:
package com.koal.util; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.koal.exception.BizException; import com.koal.web.ErrorCode; import org.apache.commons.lang3.StringUtils; import java.util.Arrays; import java.util.Optional; import java.util.regex.Pattern; /** * @author sunrj */ public class RegexUtils { /** * 對(duì)Page校驗(yàn)防止sql注入 * * @param */ public static void verifyPageFileld(Page page) { //asc校驗(yàn) Optional.ofNullable(page.ascs()).ifPresent(ascs -> { Arrays.asList(ascs).forEach(asc -> { boolean rightfulString = RegexUtils.isRightfulString(asc); if (!rightfulString) { throw new BizException(ErrorCode.COMMON_VERIFY_ERROR.getCode(), "ascs參數(shù)中含有非法的列名:" + asc); } }); }); //desc校驗(yàn) Optional.ofNullable(page.descs()).ifPresent(descs -> { Arrays.asList(descs).forEach(desc -> { boolean rightfulString = RegexUtils.isRightfulString(desc); if (!rightfulString) { throw new BizException("10011", "desc參數(shù)中含有非法的列名:" + desc); } }); }); } /** * 判斷是否為合法字符(a-zA-Z0-9-_) * * @param text * @return */ public static boolean isRightfulString(String text) { return match(text, "^[A-Za-z0-9_-]+$"); } /** * 正則表達(dá)式匹配 * * @param text 待匹配的文本 * @param reg 正則表達(dá)式 * @return */ private static boolean match(String text, String reg) { if (StringUtils.isBlank(text) || StringUtils.isBlank(reg)) { return false; } return Pattern.compile(reg).matcher(text).matches(); } }
controller校驗(yàn)page中的字段:
@GetMapping @ApiOperation(value = "查詢用戶列表", notes = "查詢用戶列表") public ServerResponse<IPage<Account>> queryAccount(Page<Account> page) { //校驗(yàn)page中的字段,防止sql注入 RegexUtils.verifyPageFileld(page); return ServerResponse.successMethod(accountService.query(page)); }
結(jié)果:
POST http://127.0.0.1:8080/account?current=1&size=10&ascs=create_time;DROP TABLE tb_account;
結(jié)果:
{
"code": "10011",
"msg": "ascs參數(shù)中含有非法的列名:create_time;DROP TABLE ag_account_info;",
"timestamp": 1653547051505
}
補(bǔ)充:Mybatis Plus自定義全局SQL注入
實(shí)現(xiàn)步驟如下:
- 在 Mapper接口中定義相關(guān)的 CRUD方法
- 擴(kuò)展 AutoSqlInjector inject 方法,實(shí)現(xiàn) Mapper接口中方法要注入的 SQL
- 在 MP全局策略中,配置 自定義注入器
① mapper中定義業(yè)務(wù)方法
如下所示:
public interface EmployeeMapper extends BaseMapper<Employee> { int deleteAll(); }
② 實(shí)現(xiàn)自己的MySqlInjector
如下所示:
/** * 自定義全局操作 */ public class MySqlInjector extends AutoSqlInjector{ /** * 擴(kuò)展inject 方法,完成自定義全局操作 */ @Override public void inject(Configuration configuration, MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) { //將EmployeeMapper中定義的deleteAll, 處理成對(duì)應(yīng)的MappedStatement對(duì)象,加入到configuration對(duì)象中。 //注入的SQL語(yǔ)句 String sql = "delete from " +table.getTableName(); //注入的方法名 一定要與EmployeeMapper接口中的方法名一致 String method = "deleteAll" ; //構(gòu)造SqlSource對(duì)象 SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); //構(gòu)造一個(gè)刪除的MappedStatement this.addDeleteMappedStatement(mapperClass, method, sqlSource); } }
③ 把自定義的MySqlInjector 配置到全局策略中
如果是xml配置方式,實(shí)例如下:
<!-- 定義MybatisPlus的全局策略配置--> <bean id ="globalConfiguration" class="com.baomidou.mybatisplus.entity.GlobalConfiguration"> <!-- 在2.3版本以后,dbColumnUnderline 默認(rèn)值就是true --> <property name="dbColumnUnderline" value="true"></property> <!-- Mysql 全局的主鍵策略 --> <property name="idType" value="0"></property>? <!-- 全局的表前綴策略配置 --> <property name="tablePrefix" value="tbl_"></property> <!--注入自定義全局操作 ?? ?--> <property name="sqlInjector" ref="mySqlInjector"></property> </bean> <!-- 定義自定義注入器 --> <bean id="mySqlInjector" class="com.jane.mp.injector.MySqlInjector"></bean>
總結(jié)
到此這篇關(guān)于Mybatis-plus sql注入及防止sql注入的文章就介紹到這了,更多相關(guān)Mybatis-plus防止sql注入內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot項(xiàng)目開(kāi)發(fā)中常用的依賴
這篇文章主要介紹了SpringBoot項(xiàng)目開(kāi)發(fā)中常用的依賴詳解,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06Java如何根據(jù)key值修改Hashmap中的value值
這篇文章主要介紹了Java如何根據(jù)key值修改Hashmap中的value值問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03java實(shí)現(xiàn)簡(jiǎn)單計(jì)算器
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)簡(jiǎn)單計(jì)算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12C++/java 繼承類的多態(tài)詳解及實(shí)例代碼
這篇文章主要介紹了C++/java 繼承類的多態(tài)詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02Java使用CompletableFuture進(jìn)行非阻塞IO詳解
這篇文章主要介紹了Java使用CompletableFuture進(jìn)行非阻塞IO詳解,CompletableFuture是Java中的一個(gè)類,用于支持異步編程和處理異步任務(wù)的結(jié)果,它提供了一種方便的方式來(lái)處理異步操作,并允許我們以非阻塞的方式執(zhí)行任務(wù),需要的朋友可以參考下2023-09-09教你如何測(cè)試Spring Data JPA的Repository
Spring Data JPA 提供了一些便捷的方式來(lái)測(cè)試這種持久層的代碼,常見(jiàn)的兩種測(cè)試類型是集成測(cè)試和單元測(cè)試,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-08-08JAVA中調(diào)用C語(yǔ)言函數(shù)的實(shí)現(xiàn)方式
這篇文章主要介紹了JAVA中調(diào)用C語(yǔ)言函數(shù)的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08