我如何防止PHP中的SQL注入?
如果用户输入未经修改而插入到SQL查询中,则该应用程序容易受到SQL注入的影响,如下例所示:
$unsafe_variable = $_POST['user_input'];
mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");
这是因为用户可以输入类似value'); DROP TABLE table;--
东西value'); DROP TABLE table;--
value'); DROP TABLE table;--
,查询变为:
INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')
可以做些什么来防止这种情况发生?
使用预准备语句和参数化查询。 这些是由数据库服务器独立于任何参数发送并解析的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);
在上面的例子中,错误模式并不是绝对必要的, 但建议添加它 。 这样,当出现Fatal Error
时,脚本不会因Fatal Error
而停止。 它使开发人员有机会catch
throw
n作为PDOException
s的任何错误。
但是, 必须强制的是第一个setAttribute()
行,它告诉PDO禁用模拟的准备语句并使用实际准备的语句。 这可以确保PHP在将语句和值发送到MySQL服务器之前不会对其进行语法分析(使攻击者无法注入恶意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
结果将仅仅是搜索字符串"'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之类的方法来提升抽象层次会更有用/更容易。
请注意,您询问的情况非常简单,而且更复杂的情况可能需要更复杂的方法。 尤其是:
mysql_real_escape_string
覆盖。 在这种情况下,您最好将用户的输入传递给白名单,以确保只允许“安全”值。 mysql_real_escape_string
方法,则在下面的注释中将会遇到Polynomial描述的问题。 这种情况很棘手,因为整数不会被引号包围,所以你可以通过验证用户输入只包含数字来处理。 这里的每个答案都只涵盖了部分问题。
实际上,我们可以动态添加四个不同的查询部分:
准备的报表只涵盖其中的2个
但有时我们必须使我们的查询更具动态性,并添加运算符或标识符。
所以,我们需要不同的保护技术。
通常,这种保护方法基于白名单。 在这种情况下,每个动态参数都应该在脚本中进行硬编码并从该组中进行选择。
例如,做动态排序:
$orders = array("name","price","qty"); //field names
$key = array_search($_GET['sort'],$orders)); // see if we have such a name
$orderby = $orders[$key]; //if not, first one will be set automatically. smart enuf :)
$query = "SELECT * FROM `table` ORDER BY $orderby"; //value is safe
但是,还有另一种方法来保护标识符 - 转义。 只要你有一个引用标识符,你可以通过将它们加倍来避开引号。
作为更进一步的一步,我们可以借用一个真正高明的想法,从准备好的语句中使用一些占位符(一个代理来表示查询中的实际值),并创建另一种类型的占位符 - 一个标识符占位符。
因此,长话短说:这是一个占位符 ,没有准备好的声明可以被视为银弹。
所以,一般的建议可能被视为
只要您使用占位符向查询中添加动态部分(并且正确处理这些占位符当然),就可以确保您的查询是安全的 。
尽管如此,SQL语法关键字存在一个问题(例如AND
, DESC
等),但白名单似乎是这种情况下唯一的方法。
更新
尽管关于SQL注入保护的最佳实践达成了普遍一致,但仍存在许多不好的做法。 其中一些根深蒂固在PHP用户的头脑中。 例如,在这个页面上(尽管大多数访问者不可见) 有80多个被删除的答案 - 由于质量差或者推广不好和过时的做法而被社区删除。 更糟糕的是,一些不好的答案并未被删除,而是繁荣起来。
例如,(1)是(2)仍然(3)很多(4)答案(5),其中包括第二个最有回报的答案,表明您手动字符串转义 - 一种过时的方法被证明是不安全的。
或者有一个稍微好一点的答案,暗示了另一种字符串格式化方法,甚至认为它是最终的灵丹妙药。 当然,事实并非如此。 这种方法并不比常规字符串格式更好,但它保留了它的所有缺点:它仅适用于字符串,并且与其他任何手动格式一样,它本质上是可选的,非强制性度量,容易出现任何类型的人为错误。
我认为所有这一切都是因为一个非常古老的迷信,得到OWASP或PHP手册等权威机构的支持,该手册宣称无论是“逃避”还是防止SQL注入都是平等的。
无论PHP手册用了多*_escape_string
, *_escape_string
绝不会使数据安全,并且从未打算过。 除了对字符串以外的任何SQL部分无用,手动转义是错误的,因为它是手动的,与自动化相反。
而且OWASP更加糟糕,强调逃避用户输入,这完全是无稽之谈:在注入保护的背景下不应该有这样的话。 每个变量都有潜在的危险 - 无论来源如何! 或者换句话说,每一个变量都必须正确格式化以便放入查询中 - 不管源再次如何。 这是重要的目的地。 当开发人员开始将山羊与山羊分开时(考虑某些特定变量是否“安全”或不),他迈出了第一步走向灾难。 更不用说,即使措辞表明批量在入口处逃脱,类似于非常神奇的报价功能 - 已经被鄙视,被弃用并被删除。
因此,与“转义”不同,准备好的语句是确实可以防止SQL注入的措施(适用时)。
如果你还不确定,下面是我写的一步一步的解释,即SQL注入预防的Hitchhiker指南,我详细解释了所有这些问题,甚至编写了一个完全致力于不良实践和其披露的部分。
链接地址: http://www.djcxy.com/p/16749.html