使用参数化查询防止 SQL 注入

SQL 注入是一种攻击,允许恶意用户修改 SQL 查询,向其中添加不需要的命令。例如,以下代码易受攻击

// Do not use this vulnerable code!
$sql = 'SELECT name, email, user_level FROM users WHERE userID = ' . $_GET['user'];
$conn->query($sql);

这允许该脚本的任何用户基本上随意修改我们的数据库。例如,考虑以下查询字符串:

page.php?user=0;%20TRUNCATE%20TABLE%20users;

这使我们的示例查询看起来像这样

SELECT name, email, user_level FROM users WHERE userID = 0; TRUNCATE TABLE users;

虽然这是一个极端的例子(大多数 SQL 注入攻击的目的不是删除数据,大多数 PHP 查询执行函数也不支持多查询),但这是一个示例,说明如何通过粗心组装来实现 SQL 注入攻击。查询。不幸的是,像这样的攻击很常见,并且由于编码人员没有采取适当的预防措施来保护他们的数据,因此非常有效。

为防止 SQL 注入发生,建议的语句是推荐的解决方案。不是将用户数据直接连接到查询,而是使用占位符。然后单独发送数据,这意味着 SQL 引擎不会将用户数据混淆为一组指令。

虽然这里的主题是 PDO,但请注意 PHP MySQLi 扩展还支持准备语句

PDO 支持两种占位符(占位符不能用于列名或表名,只能用于值):

  1. 命名占位符。冒号(:),后跟一个不同的名称(例如:user

    // using named placeholders
    $sql = 'SELECT name, email, user_level FROM users WHERE userID = :user';
    $prep = $conn->prepare($sql);
    $prep->execute(['user' => $_GET['user']]); // associative array
    $result = $prep->fetchAll();
    
  2. 传统的 SQL 位置占位符,表示为 ?

    // using question-mark placeholders
    $sql = 'SELECT name, user_level FROM users WHERE userID = ? AND user_level = ?';
    $prep = $conn->prepare($sql);
    $prep->execute([$_GET['user'], $_GET['user_level']]); // indexed array
    $result = $prep->fetchAll();
    

如果你需要动态更改表名或列名,请知道这是你自己的安全风险和不良做法。虽然,它可以通过字符串连接来完成。提高此类查询安全性的一种方法是设置允许值表,并将要连接的值与此表进行比较。

请注意,仅通过 DSN 设置连接字符集很重要,否则如果使用某些奇数编码,你的应用程序可能容易出现模糊的漏洞 。对于 5.3.6 之前的 PDO 版本,通过 DSN 设置字符集不可用,因此唯一的选择是在创建后立即在连接上将 PDO::ATTR_EMULATE_PREPARES 属性设置为 false

$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

这会导致 PDO 使用底层 DBMS 的本机预处理语句,而不仅仅是模拟它。

但是,请注意,PDO 将无声地回退到模拟 MySQL 本身无法准备的语句:它可以在手册列出源代码 )。