【Mybatis】mybatis如何处理SQL注入

2022/04/18 系统安全 中间件 共 4247 字,约 13 分钟

面试只要涉及到系统安全问题,SQL注入肯定是要问的,当然解决SQL注入问题不止一种方案,我们就以其中的一种且是我们经常使用的持久层框架mybatis进行着手,分析下如何防止SQL注入。

在分析mybatis之前,我们先看下SQL注入的定义。

SQL注入的定义是:通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。

下面我们写个demo进行实战分析,功能是查询数据库表的数据,技术栈为SpringBoot+mybatis+mysql。

数据库表

我们创建一个用户表,包含username、age等字段。

CREATE TABLE `t_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(16) NOT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1;

XML映射

我们需要根据username查询数据,mybatis查询占位符有两种,一个是#,另一个是$。

User getUser(@Param("username") String username);

#占位符

<select id="getUser" resultMap="BaseResultMap" >
  select id, username, age
  from t_user
  where username =  #{username}
</select>

$占位符

<select id="getUser2" resultMap="BaseResultMap" >
  select id, username, age
  from t_user
  where username = ${username}
</select>

测试

我们在配置文件开启打印SQL语句日志。

#查询

入参username=yindongxu,打印的SQL语句如下:

==>  Preparing: select id, username, age from t_user where username = ? 
==> Parameters: yindongxu(String)
<==    Columns: id, username, age
<==        Row: 1, yindongxu, 20
<==      Total: 1

$查询

入参username=yindongxu,打印的SQL语句如下:

==>  Preparing: select id, username, age from t_user where username = yindongxu 
==> Parameters: 

### Error querying database.  Cause: java.sql.SQLSyntaxErrorException: Unknown column 'yindongxu' in 'where clause'
### The error may exist in file [/demo/web/target/classes/mapper/UserMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: select id, username, age         from t_user         where username = yindongxu
### Cause: java.sql.SQLSyntaxErrorException: Unknown column 'yindongxu' in 'where clause'

mybatis直接把参数拼接到SQL中,并没有对参数加上”“转为字符串,而是把参数解析为表的某一列,造成SQL语法错误。

如果这里的参数是int值,那么SQL语法就正确了。

==>  Preparing: select id, username, age from t_user where username = 1 
==> Parameters: 
<==      Total: 0

$注入

SQL注入的方式有很多,我们就介绍集中简单常见的方式。

OR 1=1

如果我们在入参里加上 OR 1=1,会出现什么结果呢?

入参username='' OR 1=1, 当然,引号里面任意值都可以,这里我们使用了SQL注入,这样查询语句实际就变成了select * from table where 1=1

==>  Preparing: select id, username, age from t_user where username = '' OR 1=1
==> Parameters: 
<==    Columns: id, username, age
<==        Row: 1, yindongxu, 20
<==        Row: 2, test, 1
<==        Row: 3, happy, 30
<==      Total: 3

我们发现,结果查出user表的全量数据,这样就绕过正常的查询获取到所有数据。

Like

在需要模糊查询的时候,我们会使用 Like。

我们尝试使用#占位符,映射语句如下:

where username like  '%#{username}%'

意外发现mybatis报错,原因是映射异常,不能设置JdbcType为空的参数。

org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{property='username', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}. Cause: org.apache.ibatis.type.TypeException: Error setting non null for parameter #1 with JdbcType null .

那我们采用$占位符试下

<select id="getUserLike" resultMap="BaseResultMap" >
  select id, username, age
  from t_user
  where username like  '%${username}%'
</select>

这样是可以正常查询的,那么如何进行SQL注入呢?

入参 username = yin' OR 1=1 -- ,其中 –代表注释后面的语句,SQL日志如下:

==>  Preparing: select id, username, age from t_user where username like '%yin' OR 1=1 -- %' 
==> Parameters: 
<==    Columns: id, username, age
<==        Row: 1, yindongxu, 20
<==        Row: 2, test, 1
<==        Row: 3, happy, 30
<==      Total: 3

结果发现,这次查询获取到了表的全量数据。

当然,我们仅仅注入的仅仅是OR 1=1查询,如果注入的是删除数据库或者表的话,那就非常严重了。

模糊查询我们一般使用 Concat,同时需要指定参数的jdbcType(否则会报错)

like  Concat('%', #{username,jdbcType=VARCHAR}, '%')

$注入还有其它方法,感兴趣的可以网上搜索,自己动手去验证。

占位符使用场景

从上面测试结果可以看出,$占位符存在SQL注入的风险,那么在开发中它的使用场景在哪呢?

$使用场景

如果我们的SQL语句中有动态字段名、动态表名或者其他动态信息,可以使用$占位符,但是需要做好校验工作。

示例代码如下:

select id
  <if test="usernameColumn != null">
  , ${usernameColumn}
  </if>
  <if test="ageColumn != null">
  , ${ageColumn}
  </if>
from ${tableName}
where username = #{username}
List<User> getDynamic(@Param("usernameColumn") String usernameColumn,
                          @Param("ageColumn") String ageColumn,
                          @Param("tableName") String tableName,
                          @Param("username") String username);

userDao.getDynamic("username", null, "t_user", username);
==>  Preparing: select id , username from t_user where username = ? 
==> Parameters: test(String)
<==    Columns: id, username
<==        Row: 2, test
<==      Total: 1

我们不需要age列信息,把参数设置为null,最终的查询结果并没有包含age字段,达到我们预期的结果。

#使用场景

我们对外部的参数传递,使用#符号,这就告诉 MyBatis 创建一个预处理语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中。这样SQL注入的参数实际上就变成了一个字符串参数,就避免了注入的问题。

MyBatis是如何做到SQL预编译的呢?其实在框架底层,是JDBC中的PreparedStatement类在起作用,PreparedStatement是我们很熟悉的Statement的子类,它的对象包含了编译好的SQL语句。这种“准备好”的方式不仅能提高安全性,而且在多次执行同一个SQL时,能够提高效率。原因是SQL已编译好,再次执行时无需再编译。

总结

占位符#$
原理mybatis创建PreparedStatement预处理语句,?标识SQL中的参数参数直接拼接SQL
场景绝大部分字段参数,有效防止SQL注入动态表名、数据库名

思考题

mybatis #占位符能完全防止SQL注入么?

文档信息

搜索

    Table of Contents