开发WEB网站时,安全是个很重要的问题,其中最频繁被提到的一个问题就是SQL注入。SQL注入的原理我们就不多提了,但是提到防止SQL注入,我想很多人都会信誓旦旦地说:使用预编译和参数绑定啊,用了预编译就能防止SQL注入了。
但是,事实是,你的SQL语句真的做了预编译参数绑定吗?
真的实现了参数绑定吗
我们以SpringBoot为例,配置文件如下:
spring: profiles: active: dev datasource: type: com.zaxxer.hikari.HikariDataSource user: root password: 123456 url: jdbc:mysql://172.17.0.2:3306/test?&useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true driver-class-name: com.mysql.cj.jdbc.Driver hikari: maxLifetime: 1765000 #一个连接的生命时长(毫秒) maximumPoolSize: 15 #连接池中允许的最大连接数 pool-name: baPool connection-test-query: SELECT 1 FROM DUAL
我们使用的是beetl ORM框架,service层语句如下:
public int getCount(String name) { return userDao.getCountWithName(name); }
控制层如下:
@RequestMapping(value="/getCount") public int getCount(String name){ return userService.getCount(name); }
我们来运行一个查询,http://localhost:8080/users/getCount?name=chen
打开wireshark,来抓下包看看
![c00c292e391a801f2526fe1bb02ff839.png](https://i-blog.csdnimg.cn/blog_migrate/e4bbb3d02ade9e09ba2427f8b0973875.jpeg)
SQL查询
可以看到,实际上执行的SQL语句是
select count(*) from user where 1=1 and name = 'chen'
结果是不是很出乎意料,这执行的就是普通的SQL拼接,根本没看到参数绑定啊!难道是beetl这个框架的问题?
换成Mybatis或者Hibernate又会怎样?你可以试一下,依然用不上参数绑定和预编译!那我用最原始的JDBC原生查询呢?比如类似这种写法呢:
try ( // 因此需要在另一个方法中重新连接Connection conn = DriverManager.getConnection(url, user, pass);PreparedStatement pstmt = conn.prepareStatement("insert into student_table values(null, ?, 1)")) {for (int i = 0; i < 100; i++) {pstmt.setString(1, "姓名" + i);pstmt.executeUpdate();}System.out.println("使用PreparedStatement耗时:" + (System.currentTimeMillis() - start));}
看起来用了PreparedStatement,我要说的是,很遗憾,即使是明确使用PreparedStatement,Java里也不会做参数绑定,依然是拼接SQL查询。
不是说拼接SQL查询有SQL注入风险吗?
那要怎么做才能真正用到预编译和参数绑定呢?
实现真正的参数绑定
关键在于JDBC URL上,我们需要加这个参数:
useServerPrepStmts,只有这个参数为true时,才能做到MYSQL的参数绑定。
我把配置文件改下,注意看圈红的部分
![86937ff4552984681bbc1fb9b5a5038c.png](https://i-blog.csdnimg.cn/blog_migrate/cc8b6cfc0c7eef06f49fb8e47514b52a.jpeg)
jdbc url
再抓下包看看
![5519baf256cebaf514cbc80451ca8128.png](https://i-blog.csdnimg.cn/blog_migrate/b83c413bc1d54cc0babe3dce5f5eab9b.jpeg)
wireshark抓包MySQL参数绑定
可以看出,现在才用了真正的参数绑定,查询语句中用了 ? 占位符号,分两次发送了模板和查询参数。
实际上,JDBC预编译查询分为客户端预编译和服务器端预编译,对应的URL配置项是:useServerPrepStmts,当useServerPrepStmts为false时使用客户端(驱动包内完成SQL转义)预编译,useServerPrepStmts为true时使用数据库服务器端预编译。默认情况下,你使用的都只是客户端预编译。
客户端预编译实际上就是拼接SQL语句,但是拼接的同时,还对引号等特殊字符做了转义。
也就是说,默认情况下,不管你使用了什么ORM框架,甚至是使用原生的JDBC PreparedStatement查询,都只是做的客户端预编译,而且肯定不会用上参数绑定!
那客户端预编译和服务端预编译哪个更安全呢?那自然是服务端预编译!
那为什么几乎没人提服务端预编译呢?从抓包截图来看,服务端预编译执行了两次SQL查询,一次是发送模板,一次是发送参数,显然更耗费流量。幸运的是,同一个模板,在一个连接中只会发送一次。
那么如果没开启这个服务端预编译,会不会带来安全隐患?我们试一下,写个带有SQL注入的查询:
http://localhost:8080/users/getCount?name=chen' and salt='123
再经过Java的处理后,实际的查询是这样的
![fd7fe1052a2e3cdecdeb694ac643ef6d.png](https://i-blog.csdnimg.cn/blog_migrate/63cdc14461dc471a9ffea28f159368ac.jpeg)
SQL注入
可以看出,并没有注入成功,被转义了。也就是说,你并不需要过分担心你的SQL安全。
总结
1.不管你使用什么ORM框架或者原生JDBC查询,默认都不会使用参数绑定
2.要使用参数绑定,必须在JDBC URL中明确指定useServerPrepStmts
3.即使你不使用参数绑定,jdbc也会给你做客户端预编译来保证SQL安全。
4.如果使用原生JDBC查询,且没有使用PreparedStatement,默认配置下依然会发生SQL注入。
5.理论上,使用服务端预编译更安全,但会更耗流量。但是如果是对安全非常重视,建议你开启服务端预编译。
思考
前面提到过,客户端预编译=拼接SQL语句+对引号等特殊字符做转义,并且说不需要担心SQL注入。那么没有使用参数绑定,仅仅靠拼接SQL注入和转义,真的能做到100%的安全吗?
答案是:只要数据库连接没使用GBK字符集,目前就是100%安全,否则会存在宽字节SQL注入。服务器端预编译才是100%安全。