如果不加修改就将用户输入插入到SQL查询中,那么应用程序很容易受到SQL注入的攻击
前言
首先,我们来看一个例子
$unsafe_variable = $_POST['user_input'];
mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");
这是因为用户可以输入的值,可以变成一个drop table语句,例如:
INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')
那么我们如何避免这种情况呢?
解决思路
使用准备好的语句和参数化查询。这些SQL语句分别由数据库服务器发送和解析,与任何参数无关。这样攻击者就不可能注入恶意SQL。
有两种办法可以实现:
PDO
使用PDO(用于支持任何数据库的驱动程序)。
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute([ 'name' => $name ]);
foreach ($stmt as $row) {
// Do something with $row
}
mysqli
如果是MySQL数据库,可以使用mysqli:
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// Do something with $row
}
如果要连接到MySQL以外的数据库,可以参考一个特定于驱动程序的第二个选项(例如,pg_prepare()和pg_execute(),用于PostgreSQL)。PDO是一个兼容性更好的选择。
正确的连接
注意,在使用PDO访问MySQL数据库时,默认情况下不使用real prepared语句。要解决这个问题,必须禁用模拟准备好的语句。使用PDO创建连接的一个例子是:
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'password');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
在上面的例子中,错误模式并不是必须的,但是建议您添加它。这样,当出现错误时,脚本就不会因为致命错误而停止。并且它给开发人员机会来捕获任何作为pdoexception抛出的错误。
但是,必须执行的是第一个setAttribute()行,它告诉PDO禁用模拟的准备好的语句并使用真正的准备好的语句。这可以确保PHP在将语句和值发送到MySQL服务器之前不会对其进行解析(这使攻击者没有机会注入恶意SQL)。
虽然您可以在构造函数的选项中设置charset,但需要注意的是,PHP的“较老”版本(5.3.6之前)会忽略DSN中的charset参数。
解释
传递给prepare的SQL语句由数据库服务器解析和编译。通过指定参数(a ?或者一个命名参数,如:name(在上面的例子中),告诉数据库引擎要过滤到哪里。然后,当您调用execute时,准备好的语句与您指定的参数值相结合。
这里重要的一点是,参数值与编译后的语句相结合,而不是与SQL字符串相结合。SQL注入的工作原理是在脚本创建要发送到数据库的SQL时欺骗脚本包含恶意字符串。因此,通过将实际的SQL与参数分开发送,您可以限制以您不希望的方式结束的风险。
使用预处理语句的另一个好处是,如果在同一个会话中多次执行相同的语句,那么它只会被解析和编译一次,从而提高了一些速度。
那么如何插入数据呢,看下面这个例子(使用PDO):
$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');
$preparedStatement->execute([ 'column' => $unsafeValue ]);
动态查询
虽然您仍然可以为查询参数使用准备好的语句,但是动态查询本身的结构不能参数化,某些查询特性也不能参数化。
对于这些特定的场景,最好的方法是使用一个白名单过滤器来限制可能的值。
// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}