mybatis 是怎样防止sql注入的

最近项目交付,扫描出sql注入漏洞,我寻思spring开发框架底层应该解决了这些问题,就想研究一下是怎么解决注入的

学习mybatis时都知道 #{} 可以防注入,${}是可以注入,分别写下面两个方法

<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,否则抓包内容无法识别

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第一个请求,跟踪到

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,实际是调用

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

跟踪到

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

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分成三段

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

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

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

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

参数处理

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的预编译?

猜你喜欢

转载自juejin.im/post/7035248301083459621