Mybatis-plus sql注入及防止sql注入详解
sunrj_go 人气:0一、SQL注入是什么?
SQL注入是一种代码注入技术,用于攻击数据驱动的应用,恶意的SQL语句被 插入到执行的SQL语句中来改变查询结果,例如: OR 1=1 或者 ;drop table sys_user;等等
二、mybatis是如何做到防止sql注入的
mybatis中我们所写的sql语句都是在xml只能完成,我们在编写sql会用到 #{},${} 这个两个表达式。那 #{} 和 ${}两者之间有什么区别嘞?下面我将用两个SQL语句例子来进行说明。
<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语句中使用的#{}方式,#{}中当传入的数据是字符串,会在使用" "双引号将值引起来。
- 示例:例如 userName 传入的值是 9;DROP TABLE SYS_USER;那么#{}去取后得到的结果就是 USER_NAME="9;DROP TABLE SYS_USER;"就算传入删除表的命令也不会被执行,因为9;DROP TABLE SYS_USER;会帮当成一个完成的字符串去进行值匹配。
- 第二种SQL${}方式取值,那就变成了USER_NAME=9;DROP TABLE SYS_USER; , 因 为 ${}直接将值拼接在SQL语句后面的,使其成为SQL,因此直接将值拼接在SQL语句后面的,因此${}是存在SQL注入的风险的,在使用时要注意手动处理。
1. #{} 和 ${} 两者的区别
- #{}:解析为一个 JDBC 预编译语句,一个 #{} 被解析为一个参数占位符 ? ,#{}方式将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。 如:WHERE USER_NAME =#{username},如果传入的值是9,那么解析成sql时的值为WHERE USER_NAME =“9”,如果传入的值是12345678,则解析成的sql为WHERE USER_NAME =“12345678”,
- ${} 仅 仅 为 一 个 纯 粹 的 s t r i n g 替 换 ,${}方式传入的变量直接拼接在sql中。如:WHERE USER_NAME = ${username},如果传入的值是9,那么解析成sql时的值为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 等可以使用 ${}方式。
- #{}方式底层采用预编译方式PreparedStatement,能够很大程度防止sql注入,因为SQL注入发生在编译时;${}方式底层只是Statement,无法防止Sql注入。
$方式一般用于传入数据库对象,例如传入表名
2.PreparedStatement和Statement的区别
① PreparedStatement 在执行sql命令时,命令会先被数据库进行解析和编译,然后将其放到命令缓存区,然后,当每一个执行的相同的sql 命令时,若在缓存区发了编译命令,就不会再次进行解析和编译,这样就可以进行重复使用。PreparedStatement 在编译是会将每个#{}标记符号解析为参数参数占位符?,传入的变量就是做为参数,不会对sql语句进行修改,这样就能防止SQL注入的攻击。‘’
②Statement是直接将Sql命令直接交给数据库进行运行,不能做到拦截SQL注入的攻击,因为SQL注入时发生在运行时。Statement每次都会对SQL命令进行解析和编译,增加大数据库的开销,因此它效率不如PreparedStatement。
3.什么是预编译
预编译是做些代码文本的替换工作。是整个编译过程的最先做的工作。处理以# 开头的指令 , 比如拷贝 #include 包含的文件代码,#define 宏定义的替换 , 条件编译等,就是为编译做的预备工作的阶段。主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。而SQL注入只能发生在运行时。
4.mybaits-plus sql注入产生的原因
Mybatisplus中的 PaginationInterceptor 主要用于处理数据库的物理分页,避免内存分页。
分析PaginationInterceptor 的源码可以发现
Orderby场景下的SQL注入
前面提到了分页中会存在Orderby的使用,因为Orderby动态查询没办法进行预编译,所以不经过安全检查的话会存在注入风险。PaginationInnerInterceptor主要是通过设置com.baomidou.mybatisplus.extension.plugins.pagination.page对象里的属性来实现orderby的,主要是以下函数的调用,因为直接使用sql拼接,所以需要对进行排序的列名进行安全检查:
page.setAscs(); page.setDescs();
源码:
可以看出,分页是通过字符串拼接的方式,所以出现SQL注入的风险
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注入的
在使用分页的controller,对传入的分页插件,对ascs与descs进行检查,判断是否有非法字符,如有,则提示参数中含有非法的列名:create_time aaaa
示例:
校验字段的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 { /** * 对Page校验防止sql注入 * * @param */ public static void verifyPageFileld(Page page) { //asc校验 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参数中含有非法的列名:" + asc); } }); }); //desc校验 Optional.ofNullable(page.descs()).ifPresent(descs -> { Arrays.asList(descs).forEach(desc -> { boolean rightfulString = RegexUtils.isRightfulString(desc); if (!rightfulString) { throw new BizException("10011", "desc参数中含有非法的列名:" + desc); } }); }); } /** * 判断是否为合法字符(a-zA-Z0-9-_) * * @param text * @return */ public static boolean isRightfulString(String text) { return match(text, "^[A-Za-z0-9_-]+$"); } /** * 正则表达式匹配 * * @param text 待匹配的文本 * @param reg 正则表达式 * @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校验page中的字段:
@GetMapping @ApiOperation(value = "查询用户列表", notes = "查询用户列表") public ServerResponse<IPage<Account>> queryAccount(Page<Account> page) { //校验page中的字段,防止sql注入 RegexUtils.verifyPageFileld(page); return ServerResponse.successMethod(accountService.query(page)); }
结果:
POST http://127.0.0.1:8080/account?current=1&size=10&ascs=create_time;DROP TABLE tb_account;
结果:
{
"code": "10011",
"msg": "ascs参数中含有非法的列名:create_time;DROP TABLE ag_account_info;",
"timestamp": 1653547051505
}
补充:Mybatis Plus自定义全局SQL注入
实现步骤如下:
- 在 Mapper接口中定义相关的 CRUD方法
- 扩展 AutoSqlInjector inject 方法,实现 Mapper接口中方法要注入的 SQL
- 在 MP全局策略中,配置 自定义注入器
① mapper中定义业务方法
如下所示:
public interface EmployeeMapper extends BaseMapper<Employee> { int deleteAll(); }
② 实现自己的MySqlInjector
如下所示:
/** * 自定义全局操作 */ public class MySqlInjector extends AutoSqlInjector{ /** * 扩展inject 方法,完成自定义全局操作 */ @Override public void inject(Configuration configuration, MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) { //将EmployeeMapper中定义的deleteAll, 处理成对应的MappedStatement对象,加入到configuration对象中。 //注入的SQL语句 String sql = "delete from " +table.getTableName(); //注入的方法名 一定要与EmployeeMapper接口中的方法名一致 String method = "deleteAll" ; //构造SqlSource对象 SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); //构造一个删除的MappedStatement this.addDeleteMappedStatement(mapperClass, method, sqlSource); } }
③ 把自定义的MySqlInjector 配置到全局策略中
如果是xml配置方式,实例如下:
<!-- 定义MybatisPlus的全局策略配置--> <bean id ="globalConfiguration" class="com.baomidou.mybatisplus.entity.GlobalConfiguration"> <!-- 在2.3版本以后,dbColumnUnderline 默认值就是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>
总结
加载全部内容