问题

如果在不修改的情况下插入用户输入到SQL查询中,那么应用程序就会容易受到 SQL注入的影响,如下示例所示:

 $unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");
 

这是因为用户可以输入像value'); DROP TABLE table;--这样的东西,查询变成:

 INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')
 

可以做些什么来防止这种情况发生?

  最佳答案

使用已准备的语句和参数化查询.这些是数据库服务器发送到和解析的SQL语句,与任何参数分开.这样攻击者就不可能注入恶意的SQL.

您基本上有两个选项来实现这一目标:

  1. 使用 PDO (对于任何支持的数据库驱动程序):

     $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute(array('name' => $name));
    
    foreach ($stmt as $row) {
        // Do something with $row
    }
     
  2. 使用 MySQLi (对于MySQL):

     $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() for PostgreSQL). PDO是通用选项.

正确设置连接

注意,在使用PDO访问MySQL数据库时,默认情况下不会使用真正准备的语句.为了解决这个问题,您必须禁用已准备语句的仿真.使用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);
 

在上面的示例中,错误模式不是绝对必要的,但建议添加它.这样,当出现问题时,脚本不会停止使用Fatal Error.它给开发人员提供了catch任何错误的机会,throw n作为PDOExceptions.

但是,强制性是第一个setAttribute()行,它告诉PDO禁用已模拟的语句并使用真正准备的语句.这确保语句和值在发送到MySQL服务器之前不会被PHP解析(给可能的攻击者注入恶意SQL的机会).

虽然您可以在构造函数的选项中设置 charset,但重要的是注意到 PHP 的“旧”版本(在 5.3.6 之前) 默默地忽略了 DSN 中的 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(array('column' => $unsafeValue));
 

准备好的语句可以用于动态查询吗?

虽然仍然可以为查询参数使用已准备的语句,但动态查询本身的结构不能参数化,某些查询特性不能参数化。

对于这些特定的场景,最好的办法是使用一个限制可能值的白名单过滤器。

 // Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}
 

  相同标签的其他问题

phpmysqlsqlsecuritysql-injection