MyBatis中的SQL注入
介绍
MyBatis是Java的一个持久层框架,作用是代替JDBC对数据库进行增删改查的功能。
Mybatis的执行过程为:
- MyBatis通过读取配置文件信息(全局配置文件和映射文件),构造出SqlSessionFactory,即会话工厂。MyBatis配置文件,包括MyBatis全局配置文件和MyBatis映射文件,其中全局配置文件配置了数据源、事务等信息;映射文件配置了SQL执行相关的信息(即Mapper.xml)。
- 通过SqlSessionFactory,MyBatis可以创建SqlSession(即会话),MyBatis是通过SqlSession来操作数据库的。
- MyBatis操作数据库。SqlSession本身不能直接操作数据库,它是通过一个我们定义好的Executor接口中的方法去操作数据库,而该方法的实现就是映射文件文件中定义的(即Mapper.xml)
MyBatis的SQL语句是以xml的方式写在一个Mapper.xml文件中。
实现
假设我们有一个User表,里面有id,name,pwd三个字段:
- 定义一个MyBatisUtils工具类去获取SqlSession
package com.example.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MybatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
// 使用Mybatis第一步:获取sqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
//获取SqlSession连接实例
public static SqlSession getSession(){
return sqlSessionFactory.openSession();
}
}
- 定义User类用于传输和接收参数
package com.example.pojo;
public class User {
private int id; //id
private String name; //姓名
private String pwd; //密码
//构造,有参,无参
//set/get
//toString()
- 然后就定义Executor 接口 UserMapper,里面需要有执行的方法
package com.example.dao;
import com.example.pojo.User;
import java.util.List;
import java.util.Map;
public interface UserMapper {
// 根据id查询
User getUserById(int id);
}
- 定义映射文件配置 UserMapper.xml
<?xml version="1.0" encoding="UTF8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace=绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.example.dao.UserMapper">
<select id="getUserById" parameterType="int" resultType="com.example.pojo.User">
select * from mybatis.user where id = #{id}
</select>
- 编写测试类
@Test
public void testGetUserById() {
// 获取SqlSession连接
SqlSession session = MybatisUtils.getSession();
// 获取对应接口
UserMapper mapper = session.getMapper(UserMapper.class);
// 调用接口的方法,执行sql语句
User user = mapper.getUserById(1);
System.out.println(user);
// 关闭SqlSession连接
session.close();
}
MyBatis两种取值符号
在上面的演示实例中,只使用到了MyBatis的一种取值方式也就是#{}
这种方式的。
除了这种方式还有另外一种方式${value}
。
当parameterType为基本参数类型时,当取值符号为
$
时,{}
中的值必须为value。当parameterType为对象时,{}
中的值为对象属性名。
这两种取值符号的差别是:#{}
实现了预编译,而${}
没有!
因此在审计代码的时候只需要去留意用到了${}
的SQL语句是否存在SQL注入。
SQL注入
Mybatis框架下易产生SQL注入漏洞的情况主要分为以下三种:
模糊查询
SQL中使用%
作为通配符进行模糊查询,但是开发者使用下面这种方式拼接%
会报错
select * from mybatis.user where name like "%#{value}%"
但是将#
换成$
就不会:
select * from mybatis.user where name like "%${value}%"
但是上面的语句就能够被 a" or 1=1#
注入。正确的模糊查询语句为:
select * from mybatis.user where name like "%"#{value}"%"
// 或者
select * from mybatis.user where name like concat('%', #{value},'%')
in 之后的多个参数
in之后多个id查询时使用#
同样会报错
select * from mybatis.user where id in (#{ids})
同样地,将#
换成$
就不会:
select * from mybatis.user where id in (#{ids})
但是上面的语句就能够被 1) or 1=1#
注入。正确的查询语句为用 foreach
:
select * from mybatis.user where id in
<foreach collection="list" item="item" open="("separatosr="," close=")">
#{item}
</foreach>
无法使用#的地方
SQL语句中的一些部分,例如order by字段、表名等,是无法使用预编译语句的。
order by字段
${}
这种方式在动态排序时更加好用,比如当需要根据数据库字段id进行降序排列查询结果,#{}
由于会给传入的值自动加上引号,导致查询语句变为了select * from user order by 'id' desc
,此时会根据一个字符常量进行排序,显然不能得到我们想要的结果,此时就必须使用${}
这种方式了,因此在涉及到排序相关的业务时很容易导致sql输入的产生。
表名
表名作为变量时,必须使用 ${}
。这是因为,表名是字符串,使用 sql 占位符替换字符串时会带上单引号 ''
,这会导致 sql 语法错误,例如:
select * from #{tableName} where name = #{name}
假设我们传入的参数为 tableName = "user" , name = "John"
,那么在占位符进行变量替换后,sql 语句变为
select * from 'user' where name='John';
上述 sql 语句是存在语法错误的,表名不能加单引号 ''
(注意,反引号`是可以的)。
这时候就导致了,当MyBatis配置文件中设置allowMultiQueries=true
允许多条SQL语句执行的时候,就容易在表名处出现SQL注入。
配置文件:
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true"/>
当输入的tableName 为 user'; delete user;#
,查SQL询语句为:
select * from 'user'; delete user;#' where name = 'xxx';
修复建议
推荐开发在Java层面做映射,设置一个字段/表名数组,仅允许用户传入索引值。这样保证传入的字段或者表名都在白名单里面。(即白名单策略,不允许用户控制输入的内容)