整理了一些Java方面的架构、面试资料(微服务、集群、分布式、中间件等),有须要的小伙伴能够关注公众号【程序员内点事】,无套路自行领取javascript
更多优选java
MyBatis
是一种持久层框架,介于 JDBC
和 Hibernate
之间。经过 MyBatis 减小了手写 SQL 语句的痛苦,使用者能够灵活使用 SQL 语句,支持高级映射。可是 MyBatis 的推出不是只是为了安全问题,有不少开发认为使用了 MyBatis 就不会存在 SQL 注入了,真的是这样吗?mysql
使用了 MyBatis 就不会有 SQL 注入了吗? 答案很明显是 NO。 MyBatis它只是一种持久层框架,它并不会为你解决安全问题。固然,若是你可以遵循规范,按照框架推荐的方法开发,天然也就避免 SQL 注入问题了。本文就将 MyBatis 和 SQL 注入这些恩恩怨怨掰扯掰扯。(注本文所说的 MyBatis 默认指的是 Mybatis3)程序员
写本文的起源主要是来源于内网发现的一次 SQL 注入。咱们发现内网的一个请求的 keyword
参数存在 SQL 注入,简单地介绍一下需求背景。面试
基本上这个接口就是实现多个字段能够实现 keyword 的模糊查询,这应该是一个比较常见的需求。只不过这里存在多个查询条件。通过一番搜索,咱们发现问题的核心处于如下代码:sql
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 注入问题targetRuntime
的配置,若是不须要复杂的条件查询的话,建议直接使用 MyBatis3Simple
。这样能够更好地直接杜绝风险,由于一旦有风险点,就有发生问题的可能。做者:madneal@平安银行应用安全团队 ,查看原文
今天就说这么多,若是本文对您有一点帮助,但愿能获得您一个点赞👍哦
您的承认才是我写做的动力!
整理了一些Java方面的架构、面试资料(微服务、集群、分布式、中间件等),有须要的小伙伴能够关注公众号【程序员内点事】,无套路自行领取