PHP数据对象(PDO)扩展为PHP访问数据库定义了一个轻量级的一致接口。PDO提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可以使用相同的函数(方法)来查询和获取数据。PDO随PHP 5.1发行,在PHP 5.0的PECL扩展中也可以使用,无法运行于之前的PHP版本。
PDO多语句执行
PHP连接MySQL数据库有三种方式(MySQL、Mysqli、PDO),同时官方对三者也做了列表性比较:
Mysqli | PDO | MySQL | |
---|---|---|---|
引入的PHP版本 | 5.0 | 5.0 | 3.0之前 |
PHP5.x是否包含 | 是 | 是 | 是 |
服务端prepare语句的支持情况 | 是 | 是 | 否 |
客户端prepare语句的支持情况 | 否 | 是 | 否 |
存储过程支持情况 | 是 | 是 | 否 |
多语句执行支持情况 | 是 | 大多数 | 否 |
Mysqli通过
multi_query()
函数来进行多语句执行。
PDO使用query()
就可以进行多语句执行。
PDO默认支持多语句查询,如果php版本小于5.5.21或者创建PDO实例时未设置PDO::MYSQL_ATTR_MULTI_STATEMENTS
为false时可能会造成堆叠注入.
如果想禁止多语句执行,可在创建PDO实例时将PDO::MYSQL_ATTR_MULTI_STATEMENTS设置为false
1 | new PDO($dsn, $user, $pass, array( PDO::MYSQL_ATTR_MULTI_STATEMENTS => false)) |
PDO预处理
Mysql预处理
MySQL数据库支持预处理,预处理或者说是可传参的语句用来高效的执行重复的语句。
MySQL官方将prepare
、execute
、deallocate
统称为PREPARE STATEMENT
预制语句的SQL语法基于三个SQL语句:
1 | prepare stmt_name from preparable_stmt; |
PDO预处理
PDO分为模拟预处理和非模拟预处理。
模拟预处理是防止某些数据库不支持预处理而设置的.
在初始化PDO驱动时,可以设置一项参数,PDO::ATTR_EMULATE_PREPARES
,作用是打开模拟预处理(true)或者关闭(false),默认为true。
PDO内部会模拟参数绑定的过程,SQL语句是在最后execute()
的时候才发送给数据库执行。
一个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$dbms='mysql';
$host='192.168.27.61';
$dbName='test';
$user='root';
$pass='root';
$dsn="$dbms:host=$host;dbname=$dbName";
try {
$pdo = new PDO($dsn, $user, $pass, array( PDO::MYSQL_ATTR_MULTI_STATEMENTS => false));
} catch (PDOException $e) {
echo $e;
}
$username = $_GET['username'];
$sql = "select * from user where username = ?";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(1,$username);
$stmt->execute();
while($row=$stmt->fetch(PDO::FETCH_ASSOC))
{
var_dump($row);
echo "<br>";
}非模拟预处理则是通过数据库服务器来进行预处理动作,主要分为两步:
第一步是prepare阶段
,发送SQL语句模板到数据库服务器;
第二步通过execute()
函数发送占位符参数给数据库服务器进行执行。
非模拟预处理代码,在$username = $_GET['username'];
代码前增加$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$dbms='mysql';
$host='192.168.27.61';
$dbName='test';
$user='root';
$pass='root';
$dsn="$dbms:host=$host;dbname=$dbName";
try {
$pdo = new PDO($dsn, $user, $pass, array( PDO::MYSQL_ATTR_MULTI_STATEMENTS => false));
} catch (PDOException $e) {
echo $e;
}
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); //首先给MySQL服务器发送SQL语句模板,然后通过EXECUTE发送占位符参数给服务器
$username = $_GET['username'];
$sql = "select * from user where username = ?";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(1,$username);
$stmt->execute();
while($row=$stmt->fetch(PDO::FETCH_ASSOC))
{
var_dump($row);
echo "<br>";
}
安全问题
- 在模拟预处理下,如果SQL语句模板可控,可以控制sql语句的结构,达到多语句执行的效果。
- 在非模拟预处理下,即使SQL语句可控多语句也不可执行。
- 当设置
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
时,可利用MySQL服务端prepare时报错,然后通过设置PDO::ATTR_ERRMODE
将MySQL错误信息打印达到报错注入效果。
PDO下的宽字节注入
模拟预编译的请求发送方式和以往的mysqli并没有什么特殊区别,不同的是,在原有的查询语句中对参数并没有用单引号包裹,而在此却用单引号进行了包裹,与此同时,会将传入的单引号加上\
转义。
如果在此时设置了GBK编码,就可能产生宽字节注入的风险。
实际上,在模拟预编译的情况下,PDO对于SQL注入的防范(PDO::queto()
),无非就是将数字型的注入转变为字符型的注入,又用类似mysql_real_escape_string()
的方法将单引号、双引号、反斜杠等字符进行了转义。
例如:
1 | $query = "select * from table1 where 1=?"; |
在上面的例子中,我们可以传入id=%df' union select xxx
实现注入。
在非模拟预编译的情况下,上述问题将不会存在,它对每一句sql语句都进行了预编译和执行两个操作,在执行select * from table1 where 1=?
这句时,如果是GBK编码,那么它将会把?绑定的参数转化成16进制,这样无论输入什么样的东西都无法再进行注入了。
使用Prepare Statement方法彻底防止sql注入
PDO的原理,与Mysql中prepare语句是一样的。上面PDO所执行的SQL语句,用如下的方式可以等效替代:
1 | Set @x=0x31; |
我们可以手动将输入的参数设置为@x
,并将其转化为16进制,随后预编译,再执行
也就是说,不用PDO也可以仿照其原理手动设置预编译:
1 | $db = new mysqli('localhost','root','','pdotest'); |