mybatis 是怎样防止sql注入的

最近项目交付,扫描出sql注入漏洞,我寻思spring开发框架底层应该解决了这些问题,就想研究一下是怎么解决注入的
学习mybatis时都知道 #{} 可以防注入,${}是可以注入,分别写下面两个方法

1
2
3
4
5
6
7
8
xml复制代码<mapper namespace="com.example.ssm.mapper.UserMapper">
<select id="login1" resultType="integer">
select count(*) from user where username = #{username} and password = #{password}
</select>
<select id="login2" resultType="integer">
select count(*) from user where username = '${username}' and password = '${password}'
</select>
</mapper>

数据库连接字符串,注意别用ssl,否则抓包内容无法识别

1
bash复制代码jdbc:mysql://localhost:3306/ssm?characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false

两个 mapper 方法,根据用户名和密码查询表,如果有记录就返回 true,模拟简单的登录逻辑。前端通过 ' or '1 = 1 构造注入

image.png
login1 返回false,注入失败

image.png
login2 返回true, 注入成功

请求抓包

通过 Wireshark 来抓包,如果使用本机的数据库就选则回环网络,外网的就选联网的网卡

image.png

过滤器是 tcp.dstport == 3306 or tcp.srcport == 3306

login1 请求

8885f160c7ea5987bc9c570eee6e585.png
select count(*) from user where username = 'admin' and password = 'dd'' or ''1 = 1 '
这个sql在or两边增加了单引号,这样后面整体就是一个字符串,没有构成注入

login2 请求

f0524322a8e8d1ed027c42065b7556d.png
select count(*) from user where username = 'admin' and password = 'dd' or '1 = 1 '

sql预编译

通过debug第一个请求,跟踪到

1
2
java复制代码package org.apache.ibatis.executor.statement;
public class RoutingStatementHandler implements StatementHandler

login1 和 login2 执行过程中,在构造 StatementHandler 时选择的都是 PreparedStatementHandler
但 login1 的 boundSql 是带问号的,而 login2 的已经是拼接好参数的 sql。

7677d593df3c44d1fab3a7e9784257e.png
所以login2对已经对注入成功的sql进行预编译,就达不到防注入的效果了。
image.png

看了这个类的方法 instantiateStatement,里面有预编译的内容,打断点,可以看到会将带问号的sql进行预编译

image.png

查看 connection 的类型发现是 com.zaxxer.hikari.poolProxyConnection,实际是调用

1
2
3
java复制代码package com.mysql.cj.jdbc;
public class ConnectionImpl implements JdbcConnection, SessionEventListener, Serializable
public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException

image.png

跟踪到

1
2
3
scala复制代码package com.mysql.cj.jdbc
public class ClientPreparedStatement extends com.mysql.cj.jdbc.StatementImpl implements JdbcPreparedStatement
public ClientPreparedStatement(JdbcConnection conn, String sql, String db, ParseInfo cachedParseInfo) throws SQLException

image.png

1
2
3
arduino复制代码package com.mysql.cj
public class ParseInfo
public ParseInfo(String sql, Session session, String encoding, boolean buildRewriteInfo)

ParseInfo 会根据问号把sql分成三段,存到 byte[][] staticSql 这个二维数组中,显示的都是对应的
ASCII的十进制

问号的index会添加到 endpointList 这个数组,
image.png
然后再循环这个数组将sql分成三段

1
arduino复制代码this.staticSql = new byte[endpointList.size()][];

4d42241dd38e33fc65ce53cf8e07229.png
跟踪到最后调用底层socket发送数据

1
2
3
scala复制代码package com.mysql.cj;
public abstract class AbstractPreparedQuery<T extends QueryBindings<?>> extends AbstractQuery implements PreparedQuery<T>
public <M extends Message> M fillSendPacket(QueryBindings<?> bindings)

在发送第二段sql时,bindValues[i] 中 or (111,114) 两边会加上单引号 (39)
image.png

下面查看这个引号是怎么加上的

参数处理

1
2
3
scala复制代码package com.mysql.cj;
public class ClientPreparedQueryBindings extends AbstractQueryBindings<ClientPreparedQueryBindValue>
public void setString(int parameterIndex, String x)

有个判断 isEscapeNeededForString

image.png

这个方法是判读参数中是否有 \n \r \\ \' " \032,如果有就会循环在这些符号的位置分别处理
07de7a08b4368c5a769eb15113d4757.png
在单引号出会额外添加一个单引号,这就是我们上面发现发送第二段sql,or 的两边都加了两个单引号
image.png

总结:
mybatis在向mysql发送执行sql前,会进行客户端的预编译(还有服务器端的预编译),使用#{}表达式会将其替换为问好进行预编译,而${}则是进行参数替换后的sql进行预编译,在发送请求拼接sql时,会将参数中产生注入的地方通过处理,使其在服务器端当作一个参数,消除注入风险。在看很多文章时都再说mybatis会对sql进行预编译,但是查看抓包都是一个完整sql的请求,一步一步查找,原来是通过对sql进行切割,然后拼接处理注入风险后的参数,达到预编译的效果。

参考:

  1. wireshark抓包分析mybatis的sql参数化查询
  2. MyBatis预编译机制详解
  3. 什么是MYSQL的预编译?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%