我曾经写一篇《PDO防注入原理分析以及使用PDO的注意事项》,里面描述到php 5.3.6之前的PDO可能存在SQL注入之问题,并给出了彻底的解决方案,有的朋友给我发电子邮件,对此有疑问,说是在php 5.3.6之前版本中未发现这个漏洞。事实上这个漏洞是存在的,本文再次给出详细的演示代码。

在php 5.3.6以前版本,运行以下代码,即可发现,存在PDO SQL注入问题(可向info表中填充一些数据):<?php

$pdo = new PDO("mysql:host=127.0.0.1;dbname=test;charset=gbk","root");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->query('SET NAMES GBK');
$var = urldecode('%bf%27%20OR%20username%3Dusername%20%23');
$query = "SELECT * FROM info WHERE username = ?";
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));
$r = $stmt->fetch();
print_r($r);

而在php 5.3.6以上版本中,以上代码不存在SQL注入之问题。那么这个问题是如何产生的?如何彻底防止?

PDO支持的prepare其实有两种方式:

A.  PDO驱动本地转义 称为 native prepare 或 emulate prepare, PDO驱动将绑定的变量进行转义,再插入到SQL的占位符中,形成一个完整的SQL语句交给mysql server运行, 那么pdo用何字符集转义变量?php 5.3.6以上版本会使用DSN中的charset属性进行转义,但 php 5.3.6以前版本不支持charset选项,一律使用ascii(或latin)进行转义。

显然,在不同的php 版本中,特别是绑定变量为多字符集字符,native prepare 或 emulate prepare的行为是有差异的。

B.  利用mysql server进行转义, PDO将包含有参数占位符(问号或命名参数)的SQL发送给mysql server, 请求mysql server对SQL模板进行prepare, 然后PDO再将每个参数占位符对应的变量发送给mysql server, 由mysql server完成转义处理。我们连接mysql一般要执行set names charset, 那么mysql server将使用这个charset进行转义。显然,因为mysql server是支持多种字符集的,则不存在这个差异

意思是在 php 5.3.6以前版本中,并不支持DSN中的charset选项,所有绑定的变量均是以ascii字节进行转义的(与字符集无关)

mysql官方手册上的描述:

http://www.php.net/manual/zh/ref.pdo-mysql.connection.php

Warning

The method in the below example can only be used with character sets that share the same lower 7 bit representation as ASCII, such as ISO-8859-1 and UTF-8. Users using character sets that have different representations (such as UTF-16 or Big5) must use the charset option provided in PHP 5.3.6 and later versions.

以下两种方式任选其一可解决这个问题:

A. 通过添加(php 5.3.6以前版本):$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

B.  升级到php 5.3.6 (不用设置PDO::ATTR_EMULATE_PREPARES也可)

为了程序移植性和统一安全性,建议使用$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false)方法