整理了一些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方面的架构、面试资料(微服务、集群、分布式、中间件等),有须要的小伙伴能够关注公众号【程序员内点事】,无套路自行领取