如何防止PHP中的SQL注入?

时间:2020-03-05 18:52:36  来源:igfitidea点击:

如果将用户输入未经修改地插入到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;--')

可以采取什么措施来防止这种情况发生?

解决方案

回答

我建议使用PDO(PHP数据对象)运行参数化的SQL查询。

这不仅可以防止SQL注入,还可以加快查询速度。

而且通过使用PDO而不是mysql_,mysqli_和pgsql_函数,可以使应用程序从数据库中抽象一些,这种情况很少发生,我们必须切换数据库提供程序。

回答

Security Warning: This answer is not in line with security best practices. Escaping is inadequate to prevent SQL injection, use prepared statements instead. Use the strategy outlined below at your own risk. (Also, mysql_real_escape_string() was removed in PHP 7.)

我们可以执行以下基本操作:

$safe_variable = mysql_real_escape_string($_POST["user-input"]);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

这不会解决所有问题,但这是一个很好的垫脚石。我省略了一些明显的项目,例如检查变量的存在,格式(数字,字母等)。

回答

Warning:
  This answer's sample code (like the question's sample code) uses PHP's mysql extension, which was deprecated in PHP 5.5.0 and removed entirely in PHP 7.0.0.

如果我们使用的是最新版本的PHP,则下面概述的" mysql_real_escape_string"选项将不再可用(尽管" mysqli :: escape_string"是现代的等效项)。如今," mysql_real_escape_string"选项仅适用于旧版本PHP上的遗留代码。

我们有两个选项可以在unsafe_variable中转义特殊字符,或者使用参数化查询。两者都可以保护我们免受SQL注入的侵害。参数化查询被认为是更好的做法,但是在使用它之前,需要在PHP中更改为较新的mysql扩展。

我们将介绍先转义的较低影响字符串。

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

//Disconnect

另请参见mysql_real_escape_string函数的详细信息。

要使用参数化查询,我们需要使用MySQLi而不是MySQL函数。要重写示例,我们将需要以下内容。

<?php
    $mysqli = new mysqli("server", "username", "password", "database_name");

    // TODO - Check that connection was successful.

    $unsafe_variable = $_POST["user-input"];

    $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

我们将要阅读的关键功能将是mysqli :: prepare

另外,正如其他人所建议的那样,我们可能会发现有用/更轻松地使用PDO之类的东西来增强抽象层。

请注意,我们所询问的案例是一个相当简单的案例,而更复杂的案例可能需要更复杂的方法。特别是:

  • 如果我们想根据用户输入更改SQL的结构,则参数化查询将无济于事,并且mysql_real_escape_string没有涵盖所需的转义。在这种情况下,最好将用户的输入通过白名单,以确保仅允许"安全"值通过。
  • 如果我们在某种情况下使用用户输入中的整数并采用mysql_real_escape_string方法,则会遭受以下多项式中描述的多项式问题。这种情况比较棘手,因为整数不会被引号引起来,因此可以通过验证用户输入仅包含数字来进行处理。
  • 可能还有其他我不知道的情况。我们可能会发现这是一些有用的资源,可解决我们可能遇到的一些更细微的问题。

回答

使用准备好的语句和参数化查询。这些是独立于任何参数发送到数据库服务器并由数据库服务器解析的SQL语句。这样,攻击者就不可能注入恶意SQL。

我们基本上有两种选择可以实现此目的:

  • 使用PDO(对于任何受支持的数据库驱动程序):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');

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

foreach ($stmt as $row) {
    // do something with $row
}
  • 使用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以外的数据库,则可以参考特定于驱动程序的第二个选项(例如PostgreSQL的pg_prepare()和pg_execute())。 PDO是通用选项。

正确设置连接

注意,当使用PDO访问MySQL数据库时,默认情况下不使用真实的预处理语句。要解决此问题,我们必须禁用对准备好的语句的仿真。使用PDO创建连接的示例如下:

$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);

在上面的示例中,错误模式不是严格必需的,但建议添加它。这样,当出现问题时,脚本不会因"致命错误"而停止。并且它为开发人员提供了捕获"抛出"为" PDOException"的所有错误的机会。

但是,强制性的是第一条" setAttribute()"行,该行告诉PDO禁用模拟的准备好的语句并使用实际的准备好的语句。这可以确保在将语句和值发送到MySQL服务器之前,不会对PHP进行解析(这使可能的攻击者没有机会注入恶意SQL)。

尽管我们可以在构造函数的选项中设置charset,但要注意的一点是,PHP的"较旧"版本(<5.3.6)默默地忽略了DSN中的charset参数。

解释

发生的情况是,传递给" prepare"的SQL语句由数据库服务器解析和编译。通过指定参数(在上面的示例中为或者诸如:name之类的命名参数),我们可以告诉数据库引擎要在何处进行过滤。然后,当我们调用execute时,准备好的语句将与我们指定的参数值组合在一起。

这里重要的是参数值与已编译的语句(而不是SQL字符串)组合在一起。 SQL注入通过在创建要发送到数据库的SQL时欺骗脚本使其包含恶意字符串来起作用。因此,通过将实际的SQL与参数分开发送,可以减少因意外获得最终结果的风险。使用预处理语句发送的任何参数都将被视为字符串(尽管数据库引擎可能会进行一些优化,因此参数最终也可能以数字结尾)。在上面的示例中,如果$ name变量包含'Sarah'; DELETE FROM employee`的结果将只是搜索字符串"''Sarah'; DELETE FROM employee'",而我们将不会得到一个空表。

使用准备好的语句的另一个好处是,如果我们在同一会话中多次执行同一条语句,则它将仅被解析和编译一次,从而使我们获得了一些速度上的提高。

哦,既然我们询问了如何进行插入,这是一个示例(使用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';
}

回答

使用PDO和准备好的查询。

($ connPDO对象)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

回答

不管最终使用什么,请确保检查输入内容是否已被magic_quotes或者其他一些好听的垃圾所破坏,并在必要时通过" stripslashes"或者其他方法对其进行清理。

回答

从安全的角度来看,我赞成存储过程(MySQL从5.0开始就支持存储过程),其优点是-

  • 大多数数据库(包括MySQL)使用户访问仅限于执行存储过程。细粒度的安全访问控制对于防止特权攻击升级很有用。这样可以防止受感染的应用程序直接对数据库运行SQL。
  • 他们从应用程序中提取原始SQL查询,因此应用程序可使用的数据库结构信息较少。这使人们更难理解数据库的底层结构并设计合适的攻击。
  • 它们仅接受参数,因此存在参数化查询的优点。当然-IMO我们仍然需要清理输入-特别是如果我们在存储过程中使用动态SQL。

缺点是-

  • 它们(存储过程)很难维护并且往往会快速繁殖。这使得管理它们成为一个问题。
  • 它们不是非常适合动态查询-如果它们被构建为接受动态代码作为参数,那么许多优点就被否定了。