既然我们学会了如何提交恶意的注入语句,那么我们到底应该如何去防御注入呢?通常情况下我们可以使用以下方式来防御SQL注入攻击:
- 转义用户请求的参数值中的
'(单引号)
、"(双引号)
。 - 限制用户传入的数据类型,如预期传入的是数字,那么使用:
Integer.parseInt()/Long.parseLong
等转换成整型。 - 使用
PreparedStatement
对象提供的SQL语句预编译。
切记只过滤'(单引号)
或"(双引号)
并不能有效的防止整型注入,但是可以有效的防御字符型注入。解决注入的根本手段应该使用参数预编译的方式。
PreparedStatement SQL预编译查询
将上面存在注入的Java代码改为?(问号)
占位的方式即可实现SQL预编译查询。
示例代码片段:
// 获取用户传入的用户ID
String id = request.getParameter("id");
// 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
// 语句当中,从而导致了SQL注入漏洞。
String sql = "select id, username, email from sys_user where id =? ";
// 创建预编译对象
PreparedStatement pstt = connection.prepareStatement(sql);
// 设置预编译查询的第一个参数值
pstt.setObject(1, id);
// 执行SQL语句并获取返回结果对象
ResultSet rs = pstt.executeQuery();
需要特别注意的是并不是使用PreparedStatement
来执行SQL语句就没有注入漏洞,而是将用户传入部分使用?(问号)
占位符表示并使用PreparedStatement
预编译SQL语句才能够防止注入!
JDBC预编译
可能很多人都会有一个疑问:JDBC
中使用PreparedStatement
对象的SQL语句
究竟是如何实现预编译的?接下来我们将会以Mysql驱动包为例,深入学习JDBC
预编译实现。JDBC
预编译查询分为客户端预编译和服务器端预编译,对应的URL配置项是:useServerPrepStmts
,当useServerPrepStmts
为false
时使用客户端(驱动包内完成SQL转义)预编译,useServerPrepStmts
为true
时使用数据库服务器端预编译。
数据库服务器端预编译
JDBC URL配置示例:
jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false&useServerPrepStmts=true
代码片段:
String sql = "select host,user from mysql.user where user = ? ";
PreparedStatement pstt = connection.prepareStatement(sql);
pstt.setObject(1, user);
使用JDBC
的PreparedStatement
查询数据包如下:
客户端预编译
JDBC URL配置示例:
jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false&useServerPrepStmts=false
代码片段:
String sql = "select host,user from mysql.user where user = ? ";
PreparedStatement pstt = connection.prepareStatement(sql);
pstt.setObject(1, user);
使用JDBC
的PreparedStatement
查询数据包如下:
对应的Mysql客户端驱动包预编译代码在com.mysql.jdbc.PreparedStatement
类的setString
方法,如下:
预编译前的值为root'
,预编译后的值为'root\''
,和我们通过WireShark
抓包的结果一致。
Mysql预编译
Mysql默认提供了预编译命令:prepare
,使用prepare
命令可以在Mysql数据库服务端实现预编译查询。prepare
查询示例:
prepare stmt from 'select host,user from mysql.user where user = ?';
set @username='root';
execute stmt using @username;
查询结果如下:
mysql> prepare stmt from 'select host,user from mysql.user where user = ?';
Query OK, 0 rows affected (0.00 sec)
Statement prepared
mysql> set @username='root';
Query OK, 0 rows affected (0.00 sec)
mysql> execute stmt using @username;
+-----------+------+
| host | user |
+-----------+------+
| localhost | root |
+-----------+------+
1 row in set (0.00 sec)