PHP数据库注入的原理

本文面向对数据库注入没有充分经验的PHP工程师。当然有不足之处也请高人指正。

一个案例

在我们的业务范围内,不可避免的要接受用户传递的参数,然后拼接成SQL语句。 有这样一个API:用户传递订单号,去数据库查询这个订单的详情,然后返回给用户。 代码可能会这样写: 代码案例

<?php

$orderId = $_GET['orderId'];  
// 最简单方便的做法  
$orderInfo = $db->query("  
    SELECT  *FROM `orders` WHERE `orderId`='{$orderId}'  
");

理想情况下,用户传递的参数是简简单单的订单号,如:123456,这时候MySQL最终收到的SQL语句是:

SELECT * FROM `orders` WHERE `orderId`='123456'

可怕的是不理想的情况,比如有个不听话的用户传递的参数是:

123456'; DELETE FROM `orders` WHERE '1'='1

这种情况下,MySQL最终收到的SQL语句是:

SELECT * FROM `orders` WHERE `orderId`='JH123456'; DELETE FROM `orders` WHERE '1'='1'

可见不仅执行了查订单的语句,还清空了整个订单表,显然不是我们期望的结果。 这种由于过于相信用户传递的参数而导致执行了非期望的数据库操作的事件,称为数据库注入。 数据库注入并不是只针对MySQL。

解决方案

在公布答案前,先说说mysql_connect这个函数。这个一个落后且非常不安全的函数。从现在起,不管你通过何种途径听说到这个东西,不管有没有用过,请彻底忘记它。 目前PHP提供的一种比较稳妥的做法是:使用PDO。PDO是PHP和各种数据库之间的翻译官,它屏蔽了各种数据库的细节,抽象出了一个统一的使用方式,简单说:我们学会了PDO就学会了用PHP操作各种数据库。 同样是上一个案例,使用PDO的代码类似这样:

<?php

// 这条语句是一个模板,问号相当于一个占位符,告诉引擎接下来会用具体的值替换掉问号  
$sql = "SELECT *FROM `orders` WHERE `orderId`=?";  
$orderId = $_GET['orderId'];  
// prepare表示让引擎先熟悉下这个模板  
$instance = $pdo->prepare($sql);  
// 第一个问号占位符对应的值来了  
$instance->bindParam(1, $orderId);  
$instance->execute();

基于上面这段代码,假设用户传递的orderId参数是:JH123456 最终执行的SQL语句是:

SELECT* FROM `orders` WHERE `orderId`='JH123456'

假设那个不听话的用户传递的参数是:

JH123456'; DELETE *FROM `orders` WHERE '1'='1

最终执行的SQL语句是:

SELECT* FROM `orders` WHERE `orderId`='JH123456\'\; DELETE * FROM `orders` WHERE \'1\'=\'1'

可见非法的字符被当成普通字符来处理了,通过参数绑定的方式,完美的避免了SQL注入。

我们要做的

  • 严格过滤用户传递的参数,控制在预期范围内

  • 尽量避免简单粗暴的拼接SQL语句

  • 尽量使用PDO的prepare

  • SQL注入列入code review的审核清单