文章目录
  1. 1. mybatis 概述
  2. 2. mybatis存在的sql注入问题
  3. 3. demo演示
  4. 4. 实战分析
  5. 5. 修复方案

mybatis 概述

MyBatis是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录

mybatis存在的sql注入问题

mybatis避免了用户直接拼接SQL语句,但是认为只要使用了mybatis就可以杜绝SQL注入的观点是不正确的。mybatis在配置SQL语句的时候,有两种描述参数的方式。#{}${}

比如:

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>

这个语句被称作 selectPerson,接受一个 int(或 Integer)类型的参数,并返回一个 HashMap类型的对象,其中的键是列名,值便是结果行中的对应值。

注意参数符号:

#{id}

这就告诉 MyBatis 创建一个预处理语句参数,通过 JDBC,这样的一个参数在 SQL中会由一个“?”来标识,并被传递到一个新的预处理语句中,就像这样:

// Similar JDBC code, NOT MyBatis…
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);

使用#mybatis会使用预编译来处理参数,这样子可以避免SQL注入。

但是使用$描述参数会直接把变量拼接到SQL语句中,不做任何处理。相当于直接字符串拼接SQL。这样的方式就可能存在SQL注入。下面的描述来自mybatis官方文档。

默认情况下,使用#{}格式的语法会导致 MyBatis 创建预处理语句属性并安全地设置值(比如?)。这样做更安全,更迅速,通常也是首选做法,不过有时你只是想直接在 SQL 语句中插入一个不改变的字符串。比如,像 ORDER BY,你可以这样来使用:

ORDER BY ${columnName}
这里 MyBatis 不会修改或转义字符串。

NOTE 以这种方式接受从用户输出的内容并提供给语句中不变的字符串是不安全的,会导致潜在的 SQL 注入攻击,因此要么不允许用户输入这些字段,要么自行转义并检验。

demo演示

使用MyBatis使用示例作为demo测试。

mapper文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test.mybatis.UserMapper">
    <!-- 这里namespace必须是UserMapper接口的路径” -->
    <insert id="insertUser" parameterType="User">
        insert into user(name,age) values(#{name},#{age})
        <!-- 这里sql结尾不能加分号,否则报“ORA-00911”的错误 -->
    </insert>
    <!-- 这里的id必须和UserMapper接口中的接口方法名相同 -->
    <select id="getUser" resultType="User">
        select * from user where name=#{name}
    </select>
</mapper>

在输入中插入单引号

public static void getUser() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUser("'zhangsan");
        System.out.println("name: " + user.getName() + "|age: "
                + user.getAge());
    } finally {
        sqlSession.close();
    }

查看执行的sql语句:

可以看到输入被单引号包裹,并且对字符串中的单引号进行了转义。

#改成$,mapper文件为:

<select id="getUser" resultType="User">
    select * from user where name=${_parameter}
</select>

这里由于代码的写法问题,name需要改成_parameter,参考Mybatis中传参包There is no getter for property named XXX in class java.lang.String

查看执行的SQL语句:

输入没有任何改变的拼接到SQL中,造成了SQL注入。

实战分析

mybatis generator插件默认生成的order by 条件是使用$表示变量。

...
 <select id="selectByExample" resultMap="BaseResultMap" parameterType="com.tgwoo.ctspmt.model.MtVMsgItemExample" >
    <include refid="OracleDialectPrefix" />
    select
    <if test="distinct" >
      distinct
    </if>
    <include refid="Base_Column_List" />
    from CTSPMT.MT_V_MSG_ITEM
    <if test="_parameter != null" >
      <include refid="Example_Where_Clause" />
    </if>
    <if test="orderByClause != null" >
      order by ${orderByClause}
    </if>
    <include refid="OracleDialectSuffix" />
  </select>
...
  <sql id="OracleDialectPrefix" >
    <if test="page != null" >
      select * from ( select row_.*, rownum rownum_ from ( 
    </if>
  </sql>
  <sql id="OracleDialectSuffix" >
    <if test="page != null" >
      <![CDATA[ ) row_ ) where rownum_ > #{page.begin} and rownum_ <= #{page.end} ]]>
    </if>
  </sql>
...

所以使用mybatis generator插件生成的代码中,需要自己过滤order by参数,如果用户可控就会导致SQL注入。而大部分情况下,开发者可能没有意识到这个问题。

以freecms为例。Freecms是一款开源javacms。最新版下载地址。freecms中的orderby都是使用的$描述变量。

前面的分析可以知道,这种情况,mybatis会直接拼接字符串到SQL中不做任何处理。

注册用户登录访问:

http://192.168.99.100:8080/freecms/member/creditlog_list.do?order=extractvalue(1,concat(0x7C,(select%20user()),0x7C))

查看执行的SQL为:

修复方案

尽量使用#描述参数,如果一定要使用$,则需要自己过滤用户输入。

文章目录
  1. 1. mybatis 概述
  2. 2. mybatis存在的sql注入问题
  3. 3. demo演示
  4. 4. 实战分析
  5. 5. 修复方案