无论使用哪种数据库,避免SQL注入攻击的正确方法是将数据与SQL分离,使数据保持为数据,并且不会被SQL解析器解释为命令。可以创建一个具有正确格式化数据部分的SQL语句,但如果你不完全了解细节,应始终使用预编译语句和参数化查询。这些是将SQL语句单独发送到数据库服务器并进行解析的语句,与任何参数分开。这样一来,攻击者就无法注入恶意SQL。
基本上,你有两个选择来实现这一点:
- 使用PDO(适用于任何支持的数据库驱动程序):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute([ 'name' => $name ]);
foreach ($stmt as $row) {
// 使用$row做一些操作
}
- 使用MySQLi(适用于MySQL):
自 PHP 8.2+ 版本开始,我们可以使用execute_query()
方法来同时准备、绑定参数和执行 SQL 语句: $result = $db->execute_query('SELECT * FROM employees WHERE name = ?', [$name]);
while ($row = $result->fetch_assoc()) {
// 使用$row做一些操作
}
在 PHP8.1 及更早版本中:
$stmt = $db->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name); // 's' 指定变量类型为 'string'
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// 使用$row做一些操作
}
如果您连接的是除MySQL之外的数据库,还有一个特定于驱动程序的第二个选项可供参考(例如,对于PostgreSQL,可以使用pg_prepare()
和pg_execute()
)。PDO是通用选项。
正确设置连接
PDO
请注意,使用PDO访问MySQL数据库时,默认情况下不会使用真正的预处理语句。要解决此问题,您需要禁用模拟预处理语句。创建PDO连接的示例如下:
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8mb4', 'user', 'password');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
在上面的例子中,错误模式并不是严格必需的,但建议添加它。这样PDO将通过抛出PDOException来通知您所有的MySQL错误。
然而,第一个setAttribute()行是强制性的,它告诉PDO禁用模拟准备语句并使用真正的准备语句。这确保语句和值在发送到MySQL服务器之前不会被PHP解析(给可能的攻击者注入恶意SQL的机会)。
尽管您可以在构造函数的选项中设置字符集,但重要的是要注意,'旧版'的PHP(5.3.6之前)在DSN中默默地忽略了字符集参数。
Mysqli
对于mysqli,我们需要遵循相同的例程:
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // error reporting
$dbConnection = new mysqli('127.0.0.1', 'username', 'password', 'test');
$dbConnection->set_charset('utf8mb4'); // charset
解释
你传递给 prepare
的 SQL 语句会被数据库服务器解析和编译。通过指定参数(可以是 ?
或者像上面例子中的 :name
这样的命名参数),你告诉数据库引擎要在哪里进行过滤。然后当你调用 execute
时,预编译语句会与你指定的参数值结合。
这里重要的是,参数值与编译后的语句结合,而不是一个 SQL 字符串。SQL 注入的工作原理是通过欺骗脚本,在创建要发送到数据库的 SQL 时包含恶意字符串。因此,通过将实际的 SQL 与参数分开发送,可以减少出现意外情况的风险。
使用预编译语句时,发送的任何参数都将被视为字符串(尽管数据库引擎可能会对参数进行一些优化,使其成为数字)。在上面的例子中,如果 $name
变量包含 'Sarah'; DELETE FROM employees
,结果将仅仅是对字符串 "'Sarah'; DELETE FROM employees"
进行搜索,并且你不会得到一个空表。
使用预编译语句的另一个好处是,如果在同一会话中多次执行相同的语句,它只会被解析和编译一次,从而提供一些速度上的提升。
哦,既然你问到如何在插入数据时使用它,这里有个示例(使用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';
}