likes
comments
collection
share

mybatis 是怎样防止sql注入的

作者站长头像
站长
· 阅读数 10

最近项目交付,扫描出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 构造注入

mybatis 是怎样防止sql注入的 login1 返回false,注入失败

mybatis 是怎样防止sql注入的 login2 返回true, 注入成功

请求抓包

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

mybatis 是怎样防止sql注入的

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

login1 请求

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

login2 请求

mybatis 是怎样防止sql注入的 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。

mybatis 是怎样防止sql注入的 所以login2对已经对注入成功的sql进行预编译,就达不到防注入的效果了。 mybatis 是怎样防止sql注入的

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

mybatis 是怎样防止sql注入的

查看 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 

mybatis 是怎样防止sql注入的

跟踪到

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 

mybatis 是怎样防止sql注入的

package com.mysql.cj
    public class ParseInfo
        public ParseInfo(String sql, Session session, String encoding, boolean buildRewriteInfo)

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

问号的index会添加到 endpointList 这个数组, mybatis 是怎样防止sql注入的 然后再循环这个数组将sql分成三段

this.staticSql = new byte[endpointList.size()][];

mybatis 是怎样防止sql注入的 跟踪到最后调用底层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) mybatis 是怎样防止sql注入的

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

参数处理

package com.mysql.cj;
    public class ClientPreparedQueryBindings extends AbstractQueryBindings<ClientPreparedQueryBindValue>
        public void setString(int parameterIndex, String x) 

有个判断 isEscapeNeededForString

mybatis 是怎样防止sql注入的

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

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

参考:

  1. wireshark抓包分析mybatis的sql参数化查询
  2. MyBatis预编译机制详解
  3. 什么是MYSQL的预编译?
转载自:https://juejin.cn/post/7035248301083459621
评论
请登录