翻译自 stackoverflow

使用预处理语句参数化查询。这样的话,SQL语句会和参数分离开来,单独发送给数据库解析。使用这种方法的话,对任意攻击者来说,注入恶意的SQL都是不可能的。

你大体上有两种方式来实现上述目标:

1. 使用 PDO

1
2
3
4
5
6
7
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');

$stmt->execute(array('name' => $name));

foreach ($stmt as $row) {
// do something with $row
}

2. 使用 MySQLi

1
2
3
4
5
6
7
8
9
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name);

$stmt->execute();

$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// do something with $row
}

注意当使用 PDO 访问MySQL数据库时,真正的预处理语句默认是不启用的。要解决这个问题,你必须要关闭预处理语句模拟选项。下面是使用PDO创建数据库连接的例子:

1
2
3
4
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

上面例子中的error mode并不是必须的,但还是建议加上它。这样的话,当出错的时候,脚本不会由于发生Fatal Error而停止。而且,这样做的话让开发者有机会捕获以PDOException形式抛出的任何错误。
上面的例子中必须要有的是第一条 setAttribute() 语句,它告诉PDO关闭模拟的预处理语句,并且启用真正的预处理语句。这样做可以保证SQL语句和参数的值在发送给MySQL之前不会被PHP解析(让一切可能的攻击者没有机会注入恶意的SQL语句)。
尽管你也可以设置构造器中的charset选项,但要特别注意的是,在老版本的的PHP(<5.3.6)中, DSN默认忽略charset参数

解释
你传递给prepare的SQL语句被数据库解析并编译。通过指定参数(在上述的例子中使用?或者命名参数:name)来告诉数据库引擎你想过滤哪部分。而后,当你调用execute时,预处理语句会和你所制定的参数的值组合在一起。
需要注意的是,参数的值是和编译后的语句组合在一起,不是和SQL字符串组合在一起。SQL注入工作的方式是,当脚本生成SQL并发送给数据库时,欺骗脚本,让其包含恶意的字符串。所以通过将发送的真正的SQL和参数隔离开来,你就可以降低发生SQL注入的风险。当使用预处理语句时,你所发送的任何参数都会被当作普通的字符串处理(当然,数据库引擎可能会做一些优化,参数最后有可能转化为了数字。)
使用预处理语句的另一个好处是,如果你在同一个会话中多次执行同一条语句,它只会被解析并编译一次,这样,你可以加快脚本的运行速度。

(结束)