mybatis 中如何防止 sql 注入和传参
环境
使用 mysql,数据库名为 test,含有 1 表名为 users,users 内数据如下
JDBC 下的 SQL 注入
在 JDBC 下有两种方法执行 SQL 语句,分别是 Statement 和 PrepareStatement,即其中,PrepareStatement 为预编译
Statement
SQL 语句
当传入数据为
即当存在 username=admin 和 password=admin 的数据时则返回此用户的数据
万能密码:admin' and 1=1#
【一>所有资源获取<一】1、200 份很多已经买不到的绝版电子书 2、30G 安全大厂内部的视频资料 3、100 份 src 文档 4、常见安全面试题 5、ctf 大赛经典题目解析 6、全套工具包 7、应急响应笔记 8、网络安全学习路线
最终的 sql 语句变为了
即返回用户名为 admin,同时 1=1 的所有数据,1=1 恒为真,所以始终返回所有数据
如果输入的时:admin' or 1=1#就会返回所有数据,因为 admin' or 1=1 恒为真
所以 JDBC 使用 Statement 是不安全的,需要程序员做好过滤,所以一般使用 JDBC 的程序员会更喜欢使用 PrepareStatement 做预编译,预编译不仅提高了程序执行的效率,还提高了安全性
PreParedStatement
与 Statement 的区别在于 PrepareStatement 会对 SQL 语句进行预编译,预编译的好处不仅在于在一定程度上防止了 sql 注入,还减少了 sql 语句的编译次数,提高了性能,其原理是先去编译 sql 语句,无论最后输入为何,预编译的语句只是作为字符串来执行,而 SQL 注入只对编译过程有破坏作用,执行阶段只是把输入串作为数据处理,不需要再对 SQL 语句进行解析,因此解决了注入问题
因为 SQL 语句编译阶段是进行词法分析、语法分析、语义分析等过程的,也就是说编译过程识别了关键字、执行逻辑之类的东西,编译结束了这条 SQL 语句能干什么就定了。而在编译之后加入注入的部分,就已经没办法改变执行逻辑了,这部分就只能是相当于输入字符串被处理
而 Statement 方法在每次执行时都需要编译,会增大系统开销。理论上 PrepareStatement 的效率和安全性会比 Statement 要好,但并不意味着使用 PrepareStatement 就绝对安全,不会产生 SQL 注入。
PrepareStatement 防御预编译的写法是使用?作为占位符然后将 SQL 语句进行预编译,由于?作为占位符已经告诉数据库整个 SQL 语句的结构,即?处传入的是参数,而不会是 sql 语句,所以即使攻击者传入 sql 语句也不会被数据库解析
//预编译 sql 语句
ResultSet resultSet = pstt.executeQuery();//返回结果集,封装了全部的产部的查询结果
首先先规定好 SQL 语句的结构,然后在对占位符进行数据的插入,这样就会对 sql 语句进行防御,攻击者构造的 paylaod 会被解释成普通的字符串,我们可以通过过输出查看最终会变成什么 sql 语句
可以发现还会对单引号进行转义,一般只能通过宽字节注入,下面将会在代码的层面展示为什么预编译能够防止 SQL 注入,同时解释为什么会多出一个转义符
不安全的预编译
拼接
总所周知,sql 注入之所以能被攻击者利用,主要原因在于攻击者可以构造 payload,虽然有的开发人员采用了预编译但是却由于缺乏安全思想或者是偷懒会直接采取拼接的方式构造 SQL 语句,此时进行预编译则无法阻止 SQL 注入的产生
代码(稍稍替换一下上面的代码):
这样即使使用了预编译,但是预编译的语句已经是被攻击者构造好的语句,所以无法防御 SQL 注入
又或者是前面使用?占位符后,又对语句进行拼接,也会导致 SQL 注入
想要做到阻止 sql 注入,首先要做到使用?作为占位符,规定好 sql 语句的结构,然后在后面不破坏结构
使用 in 语句
String sql = \"delete from users where id in(\"+delIds+\");
此删除语句大多用在复选框内,在 in 当中使用拼接而不使用占位符做预编译的原因是因为很多时候无法确定 deIds 里含有多少个对象
输入:1,2
正常只会输出 id 为 1 和 2 的值
如果此时输入:1,2) or 1=1#
就会形成 SQL 注入,输出苦库里所有的值
正确写法:
还是要用到预编译,所以我们要对传入的对象进行处理,首先确定对象的个数,然后增加同量的占位符?以便预编译
以 bilibili 的删除视频为例,当我取消收藏夹复数个视频的收藏时抓到的包为
可以发现其以:2,分割,那我们只需在 split 中填写
String\[\] spl = delIds.split(\":2,\");
即可,结果为:
然后再使用预编译
使用 like 语句
当用户输入的为字符串则查询用户名和密码含有输入内容的用户信息,当用户输入的为纯数字则单查询密码,用拼接地方式会造成 SQL 注入
正常执行:
SQL 注入
正确写法
首先我们要将拼接的地方全部改为?做占位符,但是使用占位符后要使用 setString 来把传入的参数替换占位符,所以我们要先进行判断,判断需要插入替换多少个占位符
尝试进行 SQL 注入
发现被转义了
使用 order by 语句
通过上面对使用 in 关键字和 like 关键字发现,只需要对要传参的位置使用占位符进行预编译时似乎就可以完全防止 SQL 注入,然而事实并非如此,当使用 order by 语句时是无法使用预编译的,原因是 order by 子句后面需要加字段名或者字段位置,而字段名是不能带引号的,否则就会被认为是一个字符串而不是字段名,然而使用 PreapareStatement 将会强制给参数加上',我在下面会在代码层面分析为什么会这样处理参数
所以,在使用 order by 语句时就必须得使用拼接的 Statement,所以就会造成 SQL 注入,所以还要在过滤上做好防御的准备
调试分析 PrepareStatement 防止 SQL 注入的原理
进入调试,深度查看 PrepareStatement 预编译是怎么防止 sql 注入的
用户名输入 admin,密码输入 admin',目的是查看预编译如何对一个合理的字符串以及一个不合理的字符串进行处理
由于我们输入的 username 和 password 分别是 admin 和 admin',而其中 admin'属于非法值,所以我们只在
pstt.setString(2, password);
打上断点,然后步入 setString 方法
步过到 2275 行,这里有一个名为 needsQuoted 的布尔变量,默认为 true
然后进入 if 判断,其中有一个方法为 isEscapeNeededForString
步入后发现有一个布尔的 needsHexEscape,默认为 false,然后将字符串,也就是传入的参数 admin'进行逐字解析,判断是否有非法字符,如果有则置 needsHexEscape 为 true 且 break 循环,然后返回 needsHexEscape
由于我们传入的是 admin',带有'单引号,所以在 switch 的过程中会捕捉到,置 needsHexEscape = true 后直接 break 掉循环,然后直接返回 needsHexEscape
向上返回到了 setString 方法,经过 if 判断后运行 if 体里面的代码,首先创建了一个 StringBuilder,长度为传入参数即 admin+2,然后分别在参数的开头和结尾添加'
简单来说,此 switch 体的作用就是对正常的字符不做处理,直接向 StringBuilder 添加同样的字符,如果非法字符,则添加转移后的非法字符,由于不是直接的替换,而是以添加的方式,简单来说就是完全没有使用到用户传入的的参数,自然就做到了防护
我们传入的为 admin',被 switch 捕捉到后'后会在 StringBuilder 添加\和',最终我们的 admin'会变为'admin\',也就是'admin',同样防止了 SQL 注入最重要的一环--闭合语句
然后根据要插入占位符的位置进行插入
Mybatis 下的 SQL 注入
Mybatis 的两种传参方式
首先要了解在 Mybatis 下有两种传参方式,分别是{}以及#{},其区别是,使用{}的方式传参,mybatis 是将传入的参数直接拼接到 SQL 语句上,二使用 #{}传参则是和 JDBC 一样转换为占位符来进行预编译
在 #{}下运行的结果:
select * from users where username = #{username} and password = #{password}
在 ${}下运行的结果:
select * from users where username = "${username}" and password = "${password}"
SQL 注入
${}
PeopleMapper 设置
正常运行:
sql 注入:
成功 sql 注入
#{}
Mapper 设置
正常运行
尝试 SQL 注入
SQL 注入失败
使用 like 语句
正确写法
使用 in 语句
正确写法
使用 order by 语句
和 JDBC 同理,使用 #{}方式传参会导致 order by 语句失效,所以使用 order by 语句的时候还是需要做好过滤
调试分析 Mybatis 防止 SQL 注入的原理
本人学艺不精,一直定位定位不到 XMLScriptBuilder 上,所以只好看一下别人写的 mybatis 解析过程,通过解析过程来定位方法位置
先说结论,首先 Mybatis 会先对 mapper 里面的 SQL 语句进行判断,判断内容为是以 ${}传参还是以 #{}传参,如果以 #{}传参则使用?作为占位符进行预编译,Mybatis 只会对 SQL 语句的占位符做一定的处理,处理传入参数最后的步骤还是调用会 JDBC 的预编译
完整调用流程:
${}解析执行过程
首先在 XMLScriptBuilder 中的 parseDynamicNode()
在这里进行了一次判断,先说结论,这个 isDynamic 的判断其实就是判断 mapper.xml 中的 sql 语句是使用 #{}预编译还是使{}则进入 DynamicSqlSource,否则进入 RawSqlSource
进入 parseDynamicTags 方法,可以发现有两种情况会使 isDynamic 为 true,而其中 isDynamic()就是用来判定的
进入 isDynamic()
可以看到运行了两个方法,分别是 DynamicCheckerTokenParser()以及 createParser(),主要出在 createParser()上,查看方法体
发现调用了 GenericTokenParser 方法,传入了 openToken、closeToken 以及 handler 三个参数,其中 openToken 的值为{}方式传参,进入到 GenericTokenParser 方法
然而只是单纯的设置变量的值,一直向上返回到 isDynamic(),进入下一条语句 parser.parse(this.text);
在调试使就可清楚看到传入的值了,${}和 sql 语句同时出现,猜测就是在这里进行了匹配
进入到 parse 方法,此方法对 sql 语句进行解析,当遇到 ${}的字段则将此位置空(null),从返回的 StringBuilder 值可以看出
执行完后返回到 isDynamic()方法下,在 return 值递归,其实就是返回 isDynamic 的值,然后向上返回到一直返回到了 parseScriptNode()方法
最终结果就会创建一个 DynamicSqlSource 对象
至此,对 SQL 语句的解析告一段落,直到运行到 peopleMapper.getPeopleList1(people1),步入到 invoke 方法
前面的方法大致就是获取传入的参数和获取 sql 语句,步进到 execute 方法,此方法作用是判断 SQL 语句的类型
由于我们的 SQL 语句使 select,所以会落在 witch 体的 select 内,步入 case select 的 excuteForMany 方法
继续步入 selectList 方法,后面这里我不知道准确流程是怎么样的,反正经过我一番调试最终到了 query 方法这里,然后步入 getBoundSql 方法
步入 getBoundSql 方法后可以看一下参数,发现 sqlSource 的类型正是前面设置的 DynamicSqlSource
继续步入 getBoundSql 方法,然后步进到 rootSqlNode.apply 方法
这里有有个坑点啊,可能是因为我技术不够,由于这个 apply 方法有很多个实现,直接步进会跑到 MixerSqlNode 里面,但我查阅了资料发现实际上是在 TextSqlNode 里面
步入 createParser 方法,发现调用了 GenericTokenParser,这在上面解析的过程也是一样的
从 parse 方法中返回的 StringBuider 可以发现,已经成功将参数和 SQL 语句拼接在一起了
#{}解析执行过程
在前面分析{}方式传参还是使用 #{}方式传参,如果是 #{}方式则最终会调用 RawSqlSource 方法
步入 RawSqlSource 方法
继续运行,步入到 sqlSourceParser.parse 方法
可以发现出现了解析 ${}时用到的函数
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
进入方法体后发现目的是设置 openToken 和 closeToken 的值分别为 #{和}
真正对 SQL 语句进行了操作的是
String sql = parser.parse(originalSql);
步入 parser.parse 方法,运行到结尾后查看 StringBuilder 的值,发现函数把 #{}用?替换了
到此,解析过程结束,一直运行到 peopleMapper.getPeopleList1(people1),步入到 invoke 方法,然后前面的流程大致和 ${}解析一致,进入 mapperMethod.execute 方法,然后会判断执行的 sql 语句类型,然后进入 executeForMany 方法,一直运行到 selectList 方法,最后进入 query 方法
query 方法会调用自身作为返回值
在此方法的返回值又会调用 delegate.query 方法,而这个方法就是我执行 #{}的方法,进入后一直运行到
后进入
进入 queryFromDatabase 方法后运行到
进入 doQuery 方法,进入 prepareStatement()方法
其中
Connection connection = this.getConnection(statementLog);
是与数据库建立连接的对象
步入 parameterize()方法
继续步入,到 setParameters 方法
setParameters 方法的作用,是将 SQL 语句和传入的参数进行拼接
在List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();
中,获取了 boundSql,即获取到了设置的 sql 语句
ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
获取到了 SQL 语句中所需要的参数,我的 SQL 语句为 select * from users whereusername = #{username} and password = #{password},所以需要 username 和 password 两个参数
运行到
步入 setParameter 方法
在图示处打上断点,步入 setNonNullParameter 方法
继续在图示处打上断点,步入 setParameter 方法
继续在图示处打上断点,步入 setNonNullParameter 方法
虽然方法名是一样的,但是并不是同一个方法,步入 setString 方法
这里用到了动态代理,最终还是调用回了 jdbc 的 preperStatement,在图示处打上断点并步入
发现这个 setString 和上文所讲的 JDBC 的预编译使用一个函数,后面的编译方式与 JDBC 相同
Hibernate
Hibernate 执行语句的两种方法
Hibernate 可以使用 hql 来执行 SQL 语句,也可以直接执行 SQL 语句,无论是哪种方式都有可能导致 SQL 注入
Hibernate 下的 SQL 注入
HQL
hql 语句:
首先先观察一下正常登录和错误登陆下的的情况
正常登录:
错误登陆:
可以发现之间的区别在于成功登录后最后面返回了用户名
尝试进行 SQL 注入:
输入:
返回:
可以发现,经过拼接后,SQL 语句变为了
from People where username = 'admin' or '1'='1' and password = 'qwer'
说明了使用这种拼接的方式和 jdbc 以及 mybatis 是一样会产生 sql 注入的
正确写法:
正确使用以下几种 HQL 参数绑定的方式可以有效避免注入的产生
位置参数(Positional parameter)
命名参数(named parameter)
命名参数列表(named parameter list)
类实例(JavaBean)
SQL
Hibernate 支持使用原生 SQL 语句执行,所以其风险和 JDBC 是一致的,直接使用拼接的方法时会导致 SQL 注入
语句如下:Query<People> query = session.createNativeQuery("select * from user where username = '" + username + "' and password = '" + password + "'");
正确写法
调试分析 Hibernate 预防 SQL 注入原理
Hibernate 框架最终还是使用了 JDBC 中预编译防止 SQL 注入的方法
完整过程
查看一下 hibernate 预编译的过程
首先在
List\<People> list = query.list();
打下断点,步入
步入 list 方法
继续步入 list 方法
步入 doList 方法
步入 bind 方法
步入 nullSafeSet 方法
步入 getBinder 方法
最后调用的 st.setString 就是 jdbc 的 setString 方法
评论