既然我们学会了如何提交恶意的注入语句,那么我们到底应该如何去防御注入呢?通常情况下我们可以使用以下方式来防御SQL注入攻击:

  1. 转义用户请求的参数值中的'(单引号)"(双引号)
  2. 限制用户传入的数据类型,如预期传入的是数字,那么使用:Integer.parseInt()/Long.parseLong等转换成整型。
  3. 使用PreparedStatement对象提供的SQL语句预编译。

切记只过滤'(单引号)"(双引号)并不能有效的防止整型注入,但是可以有效的防御字符型注入。解决注入的根本手段应该使用参数预编译的方式。

PreparedStatement SQL预编译查询

将上面存在注入的Java代码改为?(问号)占位的方式即可实现SQL预编译查询。
示例代码片段:

  1. // 获取用户传入的用户ID
  2. String id = request.getParameter("id");
  3. // 定义最终执行的SQL语句,这里会将用户从请求中传入的host字符串拼接到最终的SQL
  4. // 语句当中,从而导致了SQL注入漏洞。
  5. String sql = "select id, username, email from sys_user where id =? ";
  6. // 创建预编译对象
  7. PreparedStatement pstt = connection.prepareStatement(sql);
  8. // 设置预编译查询的第一个参数值
  9. pstt.setObject(1, id);
  10. // 执行SQL语句并获取返回结果对象
  11. ResultSet rs = pstt.executeQuery();

需要特别注意的是并不是使用PreparedStatement来执行SQL语句就没有注入漏洞,而是将用户传入部分使用?(问号)占位符表示并使用PreparedStatement预编译SQL语句才能够防止注入!

JDBC预编译

可能很多人都会有一个疑问:JDBC中使用PreparedStatement对象的SQL语句究竟是如何实现预编译的?接下来我们将会以Mysql驱动包为例,深入学习JDBC预编译实现。
JDBC预编译查询分为客户端预编译和服务器端预编译,对应的URL配置项是:useServerPrepStmts,当useServerPrepStmtsfalse时使用客户端(驱动包内完成SQL转义)预编译,useServerPrepStmtstrue时使用数据库服务器端预编译。

数据库服务器端预编译

JDBC URL配置示例:

  1. jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false&useServerPrepStmts=true

代码片段:

  1. String sql = "select host,user from mysql.user where user = ? ";
  2. PreparedStatement pstt = connection.prepareStatement(sql);
  3. pstt.setObject(1, user);

使用JDBCPreparedStatement查询数据包如下:
3. SQL注入防御 - 图1

客户端预编译

JDBC URL配置示例:

  1. jdbc:mysql://localhost:3306/mysql?autoReconnect=true&zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useOldAliasMetadataBehavior=true&useSSL=false&useServerPrepStmts=false

代码片段:

  1. String sql = "select host,user from mysql.user where user = ? ";
  2. PreparedStatement pstt = connection.prepareStatement(sql);
  3. pstt.setObject(1, user);

使用JDBCPreparedStatement查询数据包如下:
3. SQL注入防御 - 图2

对应的Mysql客户端驱动包预编译代码在com.mysql.jdbc.PreparedStatement类的setString方法,如下:
3. SQL注入防御 - 图3
预编译前的值为root',预编译后的值为'root\'',和我们通过WireShark抓包的结果一致。

Mysql预编译

Mysql默认提供了预编译命令:prepare,使用prepare命令可以在Mysql数据库服务端实现预编译查询。
prepare查询示例:

  1. prepare stmt from 'select host,user from mysql.user where user = ?';
  2. set @username='root';
  3. execute stmt using @username;

查询结果如下:

  1. mysql> prepare stmt from 'select host,user from mysql.user where user = ?';
  2. Query OK, 0 rows affected (0.00 sec)
  3. Statement prepared
  4. mysql> set @username='root';
  5. Query OK, 0 rows affected (0.00 sec)
  6. mysql> execute stmt using @username;
  7. +-----------+------+
  8. | host | user |
  9. +-----------+------+
  10. | localhost | root |
  11. +-----------+------+
  12. 1 row in set (0.00 sec)