开发WEB网站时,安全是个很重要的问题,其中最频繁被提到的一个问题就是SQL注入。SQL注入的原理我们就不多提了,但是提到防止SQL注入,我想很多人都会信誓旦旦地说:使用预编译和参数绑定啊,用了预编译就能防止SQL注入了。至于能不能防止SQL注入,那肯定是能的。
但是,你的SQL语句真的做了预编译参数绑定吗?
1
真的实现了参数绑定吗
我们以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
我们简单点,使用jdbcTemplate来查询:
@RequestMapping("/users")
@ResponseBody
public List<Users> users(String name) {
logger.info("使用JdbcTemplate查询数据库");
String sql = "SELECT * FROM users where user_name=?";
List<Users> queryAllList = jdbcTemplate.query(sql, new Object[]{name},
new BeanPropertyRowMapper<>(Users.class));
logger.info("查询用户列表" + queryAllList);
return queryAllList;
}
我们来运行一个查询,
http://10.180.205.89:9010/users?name=admin2
打开wireshark,来抓下包看看
可以看到,实际上执行的SQL语句是
sSELECT * FROM users where user_name= 'admin2'
结果是不是很出乎意料,这执行的就是普通的SQL拼接,根本没看到参数绑定啊!难道是jdbcTemplate这个框架的问题?我不是用了?来做参数绑定了吗?我没有拼接SQL啊
换成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注入风险吗?
那要怎么做才能真正用到预编译和参数绑定呢?
2
实现真正的参数绑定
关键在于JDBC URL上,我们需要加这个参数:
useServerPrepStmts,只有这个参数为true时,才能做到MYSQL的参数绑定。
我把配置文件改下,注意区别
url: jdbc:mysql://172.17.0.2:3306/test?&useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&useServerPrepStmts=true
driver-class-name: com.mysql.cj.jdbc.Driver
再抓下包看看
可以看出,现在才用了真正的参数绑定,查询语句中用了 ? 占位符号,分两次发送了模板和查询参数。
实际上,JDBC预编译查询分为客户端预编译和服务器端预编译,对应的URL配置项是:useServerPrepStmts,当useServerPrepStmts为false时使用客户端(驱动包内完成SQL转义)预编译,useServerPrepStmts为true时使用数据库服务器端预编译。默认情况下,你使用的都只是客户端预编译。
当然,客户端的参数绑定实际是就是SQL拼接和转义。
客户端预编译实际上就是拼接SQL语句,但是拼接的同时,还对引号等特殊字符做了转义。
也就是说,默认情况下,不管你使用了什么ORM框架,甚至是使用原生的JDBC PreparedStatement查询,都只是做的客户端预编译,而且肯定不会用上参数绑定!
那客户端预编译和服务端预编译哪个更安全呢?那自然是服务端预编译!
那为什么默认不使用服务端预编译呢?从抓包截图来看,服务端预编译执行了两次SQL查询,一次是发送模板,一次是发送参数,显然更耗费流量。幸运的是,同一个模板,在一个连接中只会发送一次。
那么如果没开启这个服务端预编译,会不会带来安全隐患?我们试一下,写个带有SQL注入的查询:
http://10.180.205.89:9010/users?name='admin2
这里就不再截图了。实际上经过Java的处理后,最终的查询是被转义的。
其实不止Java,其它语言如PHP,即使用了PDO,默认配置也不会使用预编译和参数绑定。
3
总结
1. 不管你使用什么ORM框架或者原生JDBC查询,默认都不会使用SQL服务器自己的参数绑定
2.要使用服务端参数绑定,必须在JDBC URL中明确指定useServerPrepStmts
3.即使你不使用服务端参数绑定,各种jdbc框架默认配置下,如果你用了它们自己的参数绑定语法来做查询,就会给你做客户端预编译来保证SQL安全。
4. 如果使用原生JDBC查询,且没有使用PreparedStatement,默认配置下会导致SQL注入。同理,如果你在各种ORM框架里自己手动拼接SQL,也会导致SQL注入。
5.理论上,使用服务端预编译更安全,但会更耗流量。但是如果是对安全非常重视,建议你开启服务端预编译。
4
思考
前面提到过,客户端预编译=拼接SQL语句+对引号等特殊字符做转义,并且说不需要担心SQL注入。那么没有使用参数绑定,仅仅靠拼接SQL注入和转义,真的能做到100%的安全吗?
答案是:只要数据库连接没使用GBK字符集,目前就是100%安全,否则会存在宽字节SQL注入。当然,服务器端预编译是100%安全(不考虑0day或业务bug)。
长按识别二维码 关注我们
查看更多精彩内容