我怎样才能防止在PHP中的SQL注入?

如果用户输入未经修改而插入到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()PostgreSQL)。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);

在上面的例子中,错误模式并不是绝对必要的,但建议添加它。这样,Fatal Error当出现错误时,脚本不会停止。它使开发人员有机会catch被任何错误(S)thrown的PDOException秒。

什么是强制性的,但是,是第一setAttribute()线,它告诉PDO禁用模拟预处理语句和使用真正准备好的语句。这样可以确保语句和值在发送到MySQL服务器之前不会被PHP解析(让攻击者没有机会注入恶意SQL)。

虽然您可以charset在构造函数的选项中设置,但需要注意的是,较旧版本的PHP(<5.3.6)默默忽略了DSN中的charset参数

说明

会发生什么是您传递给的SQL语句prepare由数据库服务器解析和编译。通过指定参数(如上例中的?参数或命名参数: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';
}

 

/*********                 我是分割线                ***********/

 

警告: 该问题的示例代码使用PHP的mysql扩展,该扩展在PHP 5.5.0中被弃用,并在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方法,你将会遇到下面的注释中的多项式所描述的问题。这种情况是棘手的,因为整数不会被引号包围,所以你可以通过验证用户输入只包含数字来处理。
  • 有可能我还没有意识到的其他情况。您可能会发现是您可以遇到的一些更微妙的问题的有用资源。

添加评论

友情链接:蝴蝶教程