PDO的深入理解

pdo可以通过预处理语句和参数绑定防注入,那么是不是就是绝对安全呢,根据之前的开发中踩的坑以及读到的一些相关资料,做一些总结。

 

本地预处理和模拟预处理

在初始化PDO驱动时,可以设置一项参数,PDO::ATTR_EMULATE_PREPARES,作用是打开模拟预处理(true)或者关闭(false)。手册描述

模拟预处理是防止某些数据库不支持预处理,比如sqllite,这样pdo会在本地进行预处理模拟。然后会对绑定的参数进行转义处理,最后将拼接好的sql语句发送给mysql服务器进行处理。也就是预处理工作并不是发生在数据库层面。与数据库的交互只有一次,而本地预处理只是将sql模板发送给数据库,数据库完成预处理操作,包括解析,编译,生成执行计划,之后pdo再将参数发给数据完成数据绑定,换句话说,为什么native prepare statment(数据库层面的prepare)可以防止sql注入,是因为sql模板已经交由数据库解析完成,sql语意确定,那么所有的参数都会被视作字符串参数,而不是sql语句或者表达式。

如果我们使用mysql,建议关闭模拟预处理。(某些低版本mysql不支持预处理除外)。

另外注意,即使mysql版本支持预处理,某些语句mysql无法支持prepare,那么pdo在处理时还是会使用模拟预处理。这样可能会存在注入的风险。

However, be aware that PDO will silently fallback to emulating statements that MySQL can’t prepare natively,参考这篇回答

 

字符集设置的正确性

还是上面那片问答,高票答主给了一个例子

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

为什么这个例子可以完成注入,答主也给了解释,原因就是上面所说的,pdo会进行模拟预处理,这样会在本地进行转义,具体转义可能类似于mysql_real_escape_string()(PHP 5.5.0 起已废弃,并在自 PHP 7.0.0 开始被移除)或者PDO::quote(),此类函数和addslashes这种函数区别在于,它会读取当前pdo驱动设置的字符集,而set names gbk并没有设置这个值,至于为什么,看鸟哥这篇文章,所以该函数在转义时还是认为pdo驱动的字符集是latin或者是utf8,于是它给x27转义,结果就变成了xbf5c27(x27对应ascii的\),该而数据库服务器的字符集被设置成gbk,于是xbf5c27被解释成

縗'

于是完成了注入,这也就是宽字节注入,经典的5c问题。

所以除了关闭模拟预处理之外,设置字符集也很重要,除了set names utf8,还需要设置pdo驱动的字符集,防止出现fallback成模拟预处理的情况,php5.6.3版本以上可以在初始化pdo时设置,如下

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', $user, $password);

或者mysqli_character_set_name函数 。

所以说最好的做法是关闭模拟预处理和设置正确的字符集。

 

另外补充几点:
1. MySQL客户端服务端针对预处理语句和非预处理语句定义了不同的数据传输协议。预处理语句使用所谓的二进制传输协议,MySQL服务端以二进制发送数据,发送之前不会被序列化。客户端不会仅接收字符串,相反的,他们将获取二进制数据并且尝试去转换值到合适的PHP数据类型。

2. 虽然预处理语句可以帮助我们防止sql注入,但是前提是我们必须正确的使用,例如下面代码就没用了:

$sql = 'select * from tab where name=' . $name . ' and password = ? limit 1';
$pdo->prepared($sql);
$pdo->execute([$password]);

这种需要保证所有的传入参数都先使用占位符代替。

 

参考:

https://www.v2ex.com/t/362646

https://github.com/wuzehv/blog/issues/32

http://zhangxugg-163-com.iteye.com/blog/1835721