面试只要涉及到系统安全问题,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注入么?
文档信息
- 本文作者:yindongxu
- 本文链接:https://iceblow.github.io/2022/04/18/SQL%E6%B3%A8%E5%85%A5/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)