SQL参数化查询

SQL注入想必是每个人都听过,其原理和XSS攻击很相似,都是把用户的输入当做程序去执行。防御办法也很类似,就是对用户的输入进行转义,但是同样转义十分麻烦,因为SQL注入攻击的方式和变种实在太多,转义需要考虑到的情况也复杂多变;而另外一种方式就是使用参数化查询–Prepared Statements。

SQL注入

在先介绍参数化查询的时候我们先复习一下SQL注入,上面提到其原理是把用户的输入当做了SQL语句程序的一部分去执行,因为我们经常使用字符串拼接来构建SQL语句。

在这里开始演示一下(使用MySQL):

在我的数据库中我数据库中我建立了一个名为urls的表,其结构和数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> desc urls;
+-------------+------------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------------+------+-----+-------------------+----------------+
| id | int(11) unsigned | NO | PRI | NULL | auto_increment |
| url | varchar(255) | NO | | | |
| insert_time | timestamp | NO | | CURRENT_TIMESTAMP | |
| tid | bigint(20) | YES | | NULL | |
+-------------+------------------+------+-----+-------------------+----------------+
mysql> select * from urls;
+----+-----------------------+---------------------+------+
| id | url | insert_time | tid |
+----+-----------------------+---------------------+------+
| 1 | http://www.limoer.cc | 0000-00-00 00:00:00 | NULL |
| 2 | http://baidu.com/news | 0000-00-00 00:00:00 | NULL |
| 3 | http://do.io | 0000-00-00 00:00:00 | NULL |
| 5 | http://github.iod | 0000-00-00 00:00:00 | NULL |
+----+-----------------------+---------------------+------+

该表有4字段并且有4条记录,现在我们如果想要查询id=1的那条记录,应该这样写:select * from urls where id=1。执行该条语句,正确返回结果,现在我们修改一下这条语句,改成:select * from urls where id=1 and 1=1,执行这条语句,同样没问题,返回结果正常;我们接下来再把and 改成 or再执行,结果出乎我们的意料,我们把所有的记录都查询了出来,id=1的限定条件失效了。至于如何导致其失效,是因为or后面的条件1=1是恒等的,所以前面的限定条件已经不重要了,and也是如此,我们想要获取正确的结果,那么and后面的限定条件必须要正确才可以。

说到这里,其实我们就已经进行了一次SQL注入的攻击,并且窃取了数据库的所有记录(更严重的删库、窃取管理员密码也很easy)!

其实不光是上面演示到的使用and or来进行SQL注入,还有很多神奇的SQL语法让SQL注入有了可乘之机,例如我们常用的union等等。

解决办法

如果我们把上面情景放在实际开发过程中,我们可能现在有一个输入框,用户可以输入任意一个数据来查看某条记录,
服务端的SQL语句也许是这样的:select * from urls where id=${userInput}。如果某个淘气的用户不遵守约定输入了非数字,例如10 or 1=1,SQL语句拼接过后就成了这样:select * from urls where id=10 or 1=1,表中的信息一次被完全暴露!

针对上面的情况,我最想想到的不是转义输入也不是使用参数化查询,而是针对本问题,我们直接对其进行输入验证即可,既然其必须限定用户输入数字,那么在进行SQL拼接之前,对用户输入进行验证即可!

例如,在Node.js环境下,我们可以使用parseInt(userInput)就可以完成对用户输入进行强制性的验证。

第二种也就是最常用的解决办法就是转义,和防御XSS攻击一样,我们需要构建用于转义的函数,对用户的输入进行转义,还是上面的那个例子:

1
select * from urls where `id`= ${id};

如果用户输入1 or 1=1,那么毫无疑问将会导致一次非常严重的SQL注入攻击,现在假设我们已经写好了我们的转义函数escape,我们只需要在进行字符串拼接之前,做一次转义即可。 例如对于用户的输入1 or 1=1经过转义后变成了'1 or 1=1',经过SQL拼接过后则变成了:

1
select * from urls where `id`='1 or 1=1';

不出意外,我们得到了正确的结果。

关于转义函数escape如何实现,这里就不不再多说,很多数据库的驱动工具都带有相应的工具函数,我们在实际开发过程中一定要注意对用户的输入进行转义,来避免SQL注入攻击;当然,如果你使用参数化查询的话,就完全没有必要了。

参数化查询

最开始提到参数化查询的时候,我提到了Prepared Statements也就是预处理语句,其实我们可以把参数化查询理解为预处理,我们把完整的一次SQL查询分成两部分,第一步是预先查询,第二步使用参数得到结果。具体该怎么理解呢,还是接着上面的那个例子,现在我们使用参数化查询执行select * from urls where id=1。其分为两步,第一步执行select * from urls where id=?,注意这里的?,其实代表了未来将要传入的参数;第二步,传入用户的输入作为具体的id值,并且输出结果。这里要注意,因为执行完第一步的时候期待第二步传入的是一个用户的id(这里必须是数字),这时候用户传入的非法输入就不会生效,这也就从根本上杜绝了了SQL注入攻击。

好了,参数化查询(预处理)可以完全避免SQL注入,其还有其他的优点例如更加可读(相比于字符串拼接),多次查询性能会有提升(因为会对预处理语句进行缓存再利用)等。

说了这么多,那么如何使用参数化查询呢?很简单,使用一个支持该特性的数据库连接工具就可以了,比如我们下面要演示的Node环境下MySQL的参数化查询。

Demo

我们在Node环境下进行演示,首先通过npm install mysql2命令安装数据库连接工具,这里是mysql2,能够支持参数化查询。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const mysql2 = require('mysql2');
const conn = mysql2.createConnection({
host: 'localhost',
user: 'admin',
password: '123',
database: 'news'
});
// 不使用任何防护手段(将导致SQL注入攻击)
const userInput = '1 or 1=1';
conn.query(
`select * from urls where id=${userInput}`,
(err, result) => {
console.log(result);
}
);
// 使用转义(这里默认进行了转义)
conn.query(
'select * from urls where `id`=?',
['1 or 1=1'],
(err, result) => {
console.log(result);
}
);
// 使用参数化查询
conn.execute(
'select * from urls where `id`=?',
['1 or 1=1'],
(err, result, fields) => {
console.log(result);
}
);

尾巴

关于SQL注入和参数化查询就介绍到这里,如果你觉得参数化查询两步走我说得并不明确,你可以使用抓包工具来加深理解;还有最后的Demo,其实query和execute的区别就是一个支持了参数化查询而另外一个不支持;如果你运行Demo,仔细看,区别就藏在里面(Tips:B & T);最后,请总是使用参数化查询!

分享到 评论