【Mybatis进阶】SQL注⼊
SQL注⼊是⽐较常见的⽹络攻击⽅式之⼀,它不是利⽤操作系统的 Bug 来实现攻击,⽽是针对程序员编写代码时的疏忽,通过SQL语句,实现⽆账号登录,甚⾄篡改数据库等极具危害性的⾏为。
SQL注⼊攻击实例
如下是⼀个系统的账号校验的sql语句
select * from user_table where username =' ' and password=' ';
单引号⾥⾯是⽤户输⼊的⽤户名和密码。
如果⽤户输⼊ ' or 1 = 1 --作为⽤户名,上⾯的SQL语句变成:
select * from user_table where username ='' or 1 = 1 --' and password=' ';
由于⽤户名中的内容or 1=1会让sql永真,同时--是sql中的注释,可以让后⾯的判断失效。这样就可以实现⽆密码登录。
如何防⽌SQL注⼊
但凡有SQL注⼊漏洞的程序,都是因为程序要接受来⾃客户端⽤户输⼊的变量或URL传递的参数,并且这个变量或参数是组成SQL语句的⼀部分,对于⽤户输⼊的内容或传递的参数,我们应该要时刻保持警惕,这是安全领域⾥的「外部数据不可信任」的原则,纵观Web安全领域的各种攻击⽅式,⼤多数都是因为开发者违反了这个原则⽽导致的,所以⾃然能想到的,就是从变量的检测、过滤、验证下⼿,确保变量是开发者所预想的。
1、检查变量数据类型和格式
如果你的SQL语句是类似where id={$id}这种形式,数据库⾥所有的id都是数字,那么就应该在SQL被执⾏前,检查确保变量id是int类型;如果是接受邮箱,那就应该检查并严格确保变量⼀定是邮箱的格式,其他的类型⽐如⽇期、时间等也是⼀个道理。总结起来:只要是有固定格式的变量,在SQL语句执⾏前,应该严格按照固定格式去检查,确保变量是我们预想的格式,这样很⼤程度上可以避免SQL注⼊攻击。 ⽐如,我们前⾯接受username参数例⼦中,我们的产品设计应该是在⽤户注册的⼀开始,就有⼀个⽤户名的规则,⽐如5-20个字符,只能由⼤⼩写字母、数字以及⼀些安全的符号组成,不包含特殊字符。此时我们应该有⼀个check_username的函数来进⾏统⼀的检查。不过,仍然有很多例外情况并不能应⽤到这⼀准则,⽐如⽂章发布系统,评论系统等必须要允许⽤户提交任意字符串的场景,这就需要采⽤过滤等其他⽅案了。
2、过滤特殊符号
对于⽆法确定固定格式的变量,⼀定要进⾏特殊符号过滤或转义处理。
3、绑定变量,使⽤预编译语句
MySQL的mysqli驱动(没有输⼊错误,就是,mysqli)提供了预编译语句的⽀持,不同的程序语⾔,都分别有使⽤预编译语句的⽅法。实际上,绑定变量使⽤预编译语句是预防SQL注⼊的最佳⽅式,使⽤预编译的SQL语句语义不会发⽣改变,在SQL语句中,变量⽤问号?表⽰,⿊客即使本事再⼤,也⽆法改变SQL语句的结构。
什么是sql预编译
通常我们的⼀条sql在db接收到最终执⾏完毕返回可以分为下⾯三个过程:
词法和语义解析
优化sql语句,制定执⾏计划
执⾏并返回结果
我们把这种普通语句称作Immediate Statements。
但是很多情况,我们的⼀条sql语句可能会反复执⾏,或者每次执⾏的时候只有个别的值不同(⽐如query的where⼦句值不同,update 的set⼦句值不同,insert的values值不同)。如果每次都需要经过上⾯的词法语义解析、语句优化、制定执⾏计划等,则效率就明显不⾏了。
所谓预编译语句就是将这类语句中的值⽤占位符替代,可以视为将sql语句模板化或者说参数化,⼀般称这类语句叫Prepared Statements或者Parameterized Statements
预编译语句的优势在于归纳为:⼀次编译、多次运⾏,省去了解析优化等过程;此外预编译语句能防⽌sql注⼊。
MySQL的预编译功能
注意MySQL的⽼版本(4.1之前)是不⽀持服务端预编译的,但基于⽬前业界⽣产环境普遍情况,基本可以认为MySQL⽀持服务端预编译。
⼤家⼀起来看⼀个demo
第⼀步:创建数据库
CREATE TABLE `t` (
`a` int(11) DEFAULT NULL,
`b` varchar(20) DEFAULT NULL,
UNIQUE KEY `ab` (`a`,`b`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
第⼆步:通过 PREPARE stmt_name FROM preparable_stm的语法来预编译⼀条sql语句
prepare ins from 'insert into t select ?,?';
第三步:通过EXECUTE stmt_name [USING @var_name [, @var_name] ...]的语法来执⾏预编译语句
set @a=999,@b='hello';
execute ins using @a,@b;
之后我们就可以通过查询语句来查询结果。
MySQL中的预编译语句作⽤域是session级,但我们可以通过max_prepared_stmt_count变量来控制全局最⼤的存储的预编译语句。
set @@global.max_prepared_stmt_count=1;
prepare sel from 'select * from t';
简单的mysql语句当预编译条数已经达到阈值时可以看到MySQL会报如上所⽰的错误。
第四步:如果我们想要释放⼀条预编译语句,则可以使⽤{DEALLOCATE | DROP} PREPARE stmt_name的语法进⾏操作
deallocate prepare ins;
为什么PrepareStatement可以防⽌sql注⼊
原理是采⽤了预编译的⽅法,先将SQL语句中可被客户端控制的参数集进⾏编译,⽣成对应的临时变量集,再使⽤对应的设置⽅法,为临时变量集⾥⾯的元素进⾏赋值,赋值函数setString(),会对传⼊的参数进⾏强制类型检查和安全检查,所以就避免了SQL注⼊的产⽣。
前⽂中如果使⽤PrepareStatement进⾏预编译,格式会变成这样。
select * from tablename where username=? and password=?
该SQL语句会在得到⽤户的输⼊之前先⽤数据库进⾏预编译,这样的话不管⽤户输⼊什么⽤户名和密码的判断始终都是并的逻辑关系,防⽌了SQL注⼊
简单总结,参数化能防注⼊的原因在于,语句是语句,参数是参数,参数的值并不是语句的⼀部分,数据库只按语句的语义跑,⾄于跑的时候是带⼀个普通背包还是⼀个怪物,不会影响⾏进路线,⽆⾮跑的快点与慢点的区别。
mybatis是如何防⽌SQL注⼊的
说到mybatis,就不得不提到#和$的区别。⾸先看⼀下下⾯两个sql语句的区别
<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">
select id, username, password, role
from user
where username = #{username,jdbcType=VARCHAR}
and password = #{password,jdbcType=VARCHAR}
</select>
<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">
select id, username, password, role
from user
where username = ${username,jdbcType=VARCHAR}
and password = ${password,jdbcType=VARCHAR}
</select>
1、#将传⼊的数据都当成⼀个字符串,会对⾃动传⼊的数据加⼀个双引号。
如:where username=#{username},如果传⼊的值是111,那么解析成sql时的值为where username="111", 如果传⼊的值是id,则解析成的sql为where username="id"
2、$将传⼊的数据直接显⽰⽣成在sql中。
如:where username=${username},如果传⼊的值是111,那么解析成sql时的值为where username=111;
如果传⼊的值是;drop table user;,则解析成的sql为:select id, username, password, role from user where username=;drop table user;
3、#能够很⼤程度防⽌sql注⼊,$⽆法防⽌Sql注⼊。
4、$⼀般⽤于传⼊数据库对象,例如传⼊表名.
5、⼀般能⽤#的就别⽤$,若不得不使⽤${xxx}这样的参数,要⼿⼯地做好过滤⼯作,来防⽌sql注⼊攻击。
6、在MyBatis中,${xxx}这样格式的参数会直接参与SQL编译,从⽽不能避免注⼊攻击。但涉及到动态表名和列名时,只能使⽤${xxx}这样的参数格式。所以,这样的参数需要我们在代码中⼿⼯进⾏处理来防⽌注⼊。
【结论】在编写MyBatis的映射语句时,尽量采⽤#{xxx}这样的格式。若不得不使⽤${xxx}这样的参数,要⼿⼯地做好过滤⼯作,来防⽌SQL注
⼊攻击。
MyBatis框架作为⼀款半⾃动化的持久层框架,其SQL语句都要我们⾃⼰⼿动编写,这个时候当然需要防⽌SQL注⼊。其实,MyBatis的SQL 是⼀个具有“输⼊+输出”的功能,类似于函数的结构,参考上⾯的两个例⼦。其中,parameterType表⽰了输⼊的参数类型,resultType表⽰了输出的参数类型。回应上⽂,如果我们想防⽌SQL注⼊,理所当然地要在输⼊参数上下功夫。上⾯代码中使⽤#的即输⼊参数在SQL中拼接的部分,传⼊参数后,打印出执⾏的SQL语句,会看到SQL是这样的:
select id, username, password, role from user where username=? and password=?
不管输⼊什么参数,打印出的SQL都是这样的。这是因为MyBatis启⽤了预编译功能,在SQL执⾏前,会先将上⾯的SQL发送给数据库进⾏编译;执⾏时,直接使⽤编译好的SQL,替换占位符“?”就可以了。因为SQL注⼊只能对编译过程起作⽤,所以这样的⽅式就很好地避免了SQL注⼊的问题。
【底层实现原理】MyBatis是如何做到SQL预编译的呢?其实在框架底层,是JDBC中的PreparedStatement类在起作
⽤,PreparedStatement是我们很熟悉的Statement的⼦类,它的对象包含了编译好的SQL语句。这种“准备好”的⽅式不仅能提⾼安全性,⽽且在多次执⾏同⼀个SQL时,能够提⾼效率。原因是SQL已编译好,再次执⾏时⽆需再编译。
参考⽂献
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论