前言

最近在简单回顾一下sql注入,其他地方都还好,之前在面试的时候讲到sql注入的预防,我就是简单说了一下采用预编译,并没有细说预编译的一些东西,这篇文章就简单写一下Java中的预编译的代码或者原理。

写的很简单,为了更好解释一下这个预编译,其实明白之后自己往下写就很简单方便了。

SQL注入介绍

sql注入漏洞来自于sql查询语句的拼接,攻击者通过非法的输入改写sql语句的语义,以达到攻击者的目的。下面简单举一个例子

例如下面的这段代码,直接把用户post进来的id直接传入SQL查询语句,导致SQL注入产生,对于用户传进来的参数并没有进行限制以及检测。

$id=$_POST['id'];
$query="select username,email from member where id=$id";

我们直接传入一个恶意payload进去,例如像下面这样,用来查询数据库库名。

我们传入**id=2 union select 1,database()**,那么语句SQL查询语句就拼接为select username,email from member where id=2 union select 1,database()

可以发现我们前半部分查询出了select username,email from member where id=1,后半部分执行了select 1,database()

那么我们在日常开发中需要如何避免这种SQL语句由于黑客恶意构造payload导致库信息泄露甚至拖库,就要涉及到今天提及到预编译处理。

SQL语句进行预编译处理

因为sql注入是因为动态字符串的拼接导致sql命令发生改变,然后编译并且执行错误的结果。

而sql预处理则是提前“告诉”sql语法处理器,提前声明并且编译特定格式的sql语句,然后将所有用户的输入视为纯字符串参数,最后组成查询语句。

简单来说就是用事先约定好的方式进行断句,让句子不会产生歧义,达到避免部分SQL注入攻击的目的

下面就来说一下PHP用预编译来处理危险SQL语句

PHP防御SQL注入(预编译)

  1. 连接到数据库
  2. 设置预处理语句的结构
  3. 填入参数(用户输入)
  4. 执行SQL语句
<?php 
$sql_server = "localhost";
$sql_username = "root";
$sql_password = "root";
$sql_database = "pikachu";
$conn = new mysqli($sql_server, $sql_username, $sql_password, $sql_database);

// 设定预处理的语句结构 其中问号表示要填入的用户输入
$stmt = $conn->prepare('SELECT * FROM users WHERE id=?');

// 绑定参数 以变量引用形式 不宜直接填字符串进去
$id=$_POST['id'];
$stmt->bind_param("i", $id);//这里的i是指后面的变量$id是integer整型,用来限制变量$id变量

// 执行语句
$stmt->execute();


// 暂存查询的结果
$stmt->store_result();

// 取回结果个数
$res = $stmt->num_rows;

echo "共有 " . $res . " 行结果<br>";

// 绑定变量以接收结果 有多少列就要绑定多少个变量
$stmt->bind_result($id, $username, $password);

// 循环取值
while($stmt->fetch()) {
	echo $username . " : " . $password . "<br>";
}
?>

上方提到的i是integer整型,其他类型如下(时间日期等特殊数据类型,部分类型用s字符串也可以表示)

字母
s string字符串
d double双精度浮点数
i integer整型
b bool布尔型

先尝试正常查询

这时候再尝试构造恶意payload进行SQL注入攻击,可以看到构造恶意payload时只会显示id=1的结果,后方的order by 100并不会被带入执行,避免后续SQL注入攻击

那么预编译的原理和作用就很明显了,对用户的输入进行预先的规定或者检测,使用户输入的不符合规则的部分剔除掉不带入SQL查询语句即可。

Java防御SQL注入(预编译)

Java所写的SQL语句同样也是跟PHP相同的道理,比如我们代码的SQL语句是这样所写的

String id = request.getParameter("id");
String sql = "select username,password from tb_user where id = '" + id + "'";

并没有对id做限制和处理,直接传到SQL语句中就造成SQL注入,道理是跟PHP一摸一样的,就不再赘述了,直接说一下防护吧

  1. 转义用户请求的参数值中的’(单引号)、”(双引号)。

  2. 限制用户传入的数据类型,如预期传入的是数字,那么使用**:Integer.parseInt()/Long.parseLong**等转换成整型,这地方跟上方PHP差不多,限制一下传入的数据格式

  3. 使用PreparedStatement对象提供的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预编译的一些知识

JDBC预编译查询分为客户端预编译和服务器端预编译,对应的URL配置项是:useServerPrepStmts,当useServerPrepStmts为false时使用客户端(驱动包内完成SQL转义)预编译,useServerPrepStmts为true时使用数据库服务器端预编译。

MySQL预编译

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

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)

总结

  1. 预处理语句事先约定一次或多次规则,可以有效防止SQL注入
  2. 预处理语句占用带宽小,一次约定,多次查询,只需要向服务器发送绑定的参数,而不是整个sql语句
  3. 虽然预处理能够有效防止漏洞,但是仍然要在前后端接收的时候做好字符的验证与过滤,保证整个接口的完整性