【系列目录】
Mybatis源码阅读之一——工厂模式与SqlSessionFactory
Mybatis源码阅读之二——模板方法模式与Executor
Mybatis源码阅读之三——JDBC解析与Mybatis封装
Mybatis源码阅读之四——装饰器模式与Mybatis中的各种Cache
Mybatis源码阅读之六——数据库连接池实现与hikariCP简析
Mybatis源码阅读之八——代理模式与Mybatis插件(pagehelper为例)
Mybatis源码阅读之十——Mybatis中优雅的责任链模式
文章目录
Sql注入是前些年盛行的一种服务攻击手段,是发生于应用程序与数据库层的安全漏洞。
注入举例
原功能SQL如下,这是一个根据username查询的sql。
select * from admin where username='$username'
一个正常的请求如下:
select * from admin where username="zhangsan"
但是如果没有预防措施,这里的参数$username有可能篡改,造成数据泄漏/恶意错误/恶意阻塞等问题。
payload:
username=admin' union select * from sys_user
恶意SQL:
select * from admin where username='admin' or 1 union select * from sys_user
注入手段
攻击者是如何知道某个接口有SQL可注入漏洞。
错误回显
通过页面传参测试生成如下Sql:
payload:
url?username='"
select * from admin where
username='"
显然这是一个左单引号有双引号的错误sql,如果接口返回SQL语法的错误信息,那么就可以明确的知道这个接口没有做SQL注入的防御措施。
那我们可能会说,既然错误信息提示了攻击者,那我们把SQL错误的内部信息拦截掉,不展示到页面不就好了?
答案没那么简单。
盲注
没有错误信息,攻击者还可以通过盲注进行接口是否可注入的验证。
构造简单的条件语句,根据返回页面是否发生变化来判断SQL语句是否得到执行。
比如:
payload:
url?username=真实username and 1=2
当我们关掉了错误回显,以上sql的执行显然会返回一个空结果。但是做了防注入处理的执行也可能返回空结果,所以需要进一步验证。
payload:
url?username=真实username and 1=1
如果这一次能够正常看到数据,那么说明漏洞存在。
Time Attach
除了盲注还有一种判断方式,利用BENCHMARK函数或SLEEP函数。
payload:
url?id=1 and (select sleep(5))
select * from admin where
id=1 and (select sleep(5))
如果接口响应达到我们指定的睡眠秒数,那么可以认定存在漏洞。
参考: 《白帽子讲Web安全》
如何防止注入
一条sql的行程
在一个使用Mybatis的Web应用中,sql会经过如图过程,可见,哪里会对SQL进行参数解析赋值,哪里就可能产生注入问题。
Mybatis(或其他JDBC封装)层面防注入
那么这里就不只是要考虑对Mysql服务本身的注入,还要考虑使用Mybatis等对JDBC功能进行了封装的框架,它们可能导致的注入问题。
占 位 符 , 这 是 一 个 常 见 面 试 题 , 这 里 的 漏 洞 无 法 在 数 据 库 层 面 防 范 。 ∗ ∗ 因 为 它 发 生 在 M y b a t i s 对 S Q L 的 解 析 过 程 中 , 会 将 {}占位符,这是一个常见面试题,这里的漏洞无法在数据库层面防范。 **因为它发生在Mybatis对SQL的解析过程中,会将 占位符,这是一个常见面试题,这里的漏洞无法在数据库层面防范。∗∗因为它发生在Mybatis对SQL的解析过程中,会将{}替换成对应的参数值,得到一个完整的SQL,然后使用完整SQL再请求数据库。**
我们应该尽量使用#{},#{}不会在Mybatis中直接替换成完整SQL,而会使用数据库提供的预编译功能来防止SQL注入。
Mysql层面防注入
为了从根本上避免上述注入问题,我们应该使用数据库本身提供的预编译机制。
JDBC对预编译也提供了对应的抽象接口(PerparedStatement):
预编译实现参数化查询
什么是数据库预编译?
将一次Sql拆分为多次,第一次传入Sql,不包含参数;第二次传入参数,执行Sql。
一个插入sql的预编译使用流程如下:
mysql> prepare goodstest from 'insert into goods values(?,?)';
Query OK, 0 rows affected (0.00 sec)
Statement prepared
mysql> set @a=1,@b='测试商品';
Query OK, 0 rows affected (0.00 sec)
mysql> execute ins using @a,@b;
Query OK, 1 row affected (0.01 sec)
Records: 1 Duplicates: 0 Warnings: 0
为什么预编译可以防止Sql注入?
因为数据库(Mysql)服务本身在第一次请求后已经知道了这个SQL本身结构,知道?的位置是参数,那么数据库就可以对第二次传入的参数做限制,无论第二次过来的是什么语句,包含什么关键字都没关系,我认为你就是个字符串或其他基本类型。
不可以预编译解决的注入-白名单
预编译不是万能的,有些场景下就不可以使用,比如order by {参数变量}。
为什么order by {参数变量}不能使用预编译,不能使用#{}?
因为预编译本身是对传入的参数做了字符串化的操作,也就是将传入的参数前后都加上一个引号,这种处理对于参数来说自然是可以的,因为数据库会进行自动的类型转换,但是对于order by却不可以,它后面跟的不是参数,而是表字段/关键字。
如果带上引号成了order by ‘username’,那username就是一个字符串不是字段名了,那么排序自然不成立。对于mysql来说,它将会无视这个order by条件。
那么我们就可以得出一个结论:
凡是字符串但又不能加引号的位置都不能参数化,包括sql关键字、库名表名字段名函数名等等。
那么这种情况下,该如何解决呢?
在应用服务中手动添加白名单,比如我们可以根据id,name排序,那么就在业务代码中过滤判断进行拦截。
当然白名单也可以做的优雅一些,我们可以借助数据库可查询库表字段的功能来实现。
原创不易,文章下方点个在看,作者更有动力!