聊一聊MyBatis 和 SQL 注入间的恩恩怨怨

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

更多优选java

引言

MyBatis 是一种持久层框架,介于 JDBCHibernate 之间。经过 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_nameorgstatus 以及 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,所以只是添加了一个只有 conditionCriterion。如今再来看 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 服务器端已经支持了预编译功能。

不少主流持久层框架(MyBatisHibernate) 其实都没有真正的用上预编译,预编译是要咱们本身在参数列表上面配置的,若是咱们不手动开启,JDBC 驱动程序 5.0.5 之后版本 默认预编译都是关闭的。

须要经过配置参数来进行开启:

jdbc:mysql://localhost:3306/mybatis?&useServerPrepStmts=true&cachePrepStmts=true

数据库 SQL 执行包含多个阶段以下图所示,但咱们这里针对于 SQL 语句客户端的预编译在发送到服务端以前就已经完成了。在服务器端主要考虑的就是性能问题,这不是本文的重点。

固然,每个数据库实现的预编译方式可能都有一些差异。可是对于防止 SQL 注入,在 MyBatis 中只要使用 #{} 就能够了,由于这样就会实现 SQL 语句的参数化,避免直接引入恶意的 SQL 语句并执行。

在这里插入图片描述

MyBatis generator 的使用

对于使用 MyBatisMyBatis 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种配置项,即 MyBatis3MyBatis3Simple,MyBatis3 为默认配置项。MyBatis3Simple 只会生成基本的增删改查,而 MyBatis3 会生成带条件的增删改查,全部的条件都在 XXXexample 中封装。

使用 MyBatis3 时,enableSelectByExampleenableDeleteByExampleenableCountByExample 以及 enableUpdateByExample 这些属性为 true,就会生成相应的动态语句。这也就是咱们上述 Example_Where_Clause 生成的缘由。

若是使用配置项 MyBatis3Simple,那么生成的 SQL Map XML 文件将很是简单,只包含一些基本的方法,也不会产生上面的动态方法。能够这么说,若是你使用 MyBatis3Simple 话,而且不额外改造,由于里面全部的变量都是经过 #{} 引入,就不可能会有 SQL 注入的问题。

可是现实业务中每每涉及到复杂的查询条件,并且通常开发使用的都是祖传配置文件,因此究竟是使用 MyBatis3 仍是 MyBatis3Simple,仍是须要具体问题,具体看待。不过若是你是使用默认配置,你就须要小心了,谨记一点,外部传入的参数是极有多是不安全的,是不能够直接引入处理的。意思到这一点,就基本能够很好地避免 SQL 注入问题了。

总结

这篇文章从内网的一个 SQL 注入漏洞引起的对 MyBatis 的使用问题思考,对 MyBatis 中 #{} 工做的原理以及 Mybatis generator 的使用多个方面作了进一步的思考。

能够总结如下几点:

  • 能不使用拼接就不要使用拼接,这应该也是避免 SQL 注入最基本的原则
  • 在使用 ${} 传入变量的时候,必定要注意变量的引入和过滤,避免直接经过 ${} 传入外部变量
  • 不要本身造轮子,尤为是在安全方面,其实在这个问题上,框架已经提供了标准的方法。若是按照规范开发的话,也不会致使 SQL 注入问题
  • 能够注意 MyBatis 中 targetRuntime 的配置,若是不须要复杂的条件查询的话,建议直接使用 MyBatis3Simple。这样能够更好地直接杜绝风险,由于一旦有风险点,就有发生问题的可能。

做者:madneal@平安银行应用安全团队 ,查看原文


今天就说这么多,若是本文对您有一点帮助,但愿能获得您一个点赞👍哦

您的承认才是我写做的动力!


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

相关文章
相关标签/搜索