跳转到内容

PHP 编程/SQL 注入攻击

来自维基教科书,面向开放世界的开放书籍

考虑以下 PHP 中的 SQL 查询

$result=mysql_query('SELECT * FROM users WHERE username="'.$_GET['username'].'"');

该查询从 users 表中选择所有用户名等于查询字符串中输入的用户名。如果你仔细观察,你会发现该语句容易受到 SQL 注入的攻击 - $_GET['username'] 中的引号没有转义,因此将被连接为语句的一部分,这可能导致恶意行为。

考虑如果 $_GET['username'] 为以下内容会发生什么:" OR 1 OR username = "(一个双引号,后跟一个文本“OR 1 OR username =”,然后是另一个双引号)。当连接到原始表达式时,你将得到一个如下所示的查询:SELECT * FROM users WHERE username = "" OR 1 OR username = ""。看似多余的 OR username = " 部分是为了确保 SQL 语句在没有错误的情况下进行评估。否则,在语句末尾会留下一个悬空的双引号。

这将从 users 表中选择所有行。

解决方案

[编辑 | 编辑源代码]

输入验证

[编辑 | 编辑源代码]

永远不要相信用户提供的数据,只有在验证后才处理这些数据;按照惯例,这是通过模式匹配来完成的。在下面的示例中,用户名被限制为字母数字字符加下划线,长度在 8 到 20 个字符之间 - 需根据需要修改。

 if (preg_match("/^\w{8,20}$/", $_GET['username'], $matches))
   $result = mysql_query("SELECT * FROM users WHERE username='$matches[0]'");
 else // we don't bother querying the database
   echo "username not accepted";

为了提高安全性,你可能需要使用 exit()die() 来中止脚本的执行,而不是使用 echo

此问题仍然适用于使用复选框、单选按钮、下拉列表等情况。任何浏览器请求(即使是 POST)都可以通过 telnet、复制站点、javascript 或代码(甚至 PHP)来复制,因此始终谨慎处理对客户端代码设置的任何限制。

转义值

[编辑 | 编辑源代码]

PHP 提供了一个函数来处理 MySQL 中的用户输入,即 mysqli_real_escape_string([mysqli link, ]string unescaped_string)。此脚本转义字符串中所有潜在的危险字符,并返回转义后的字符串,使其可以安全地放入 MySQL 查询中。但是,如果你在将输入传递给 mysqli_real_escape_string() 函数之前没有对其进行消毒,你仍然可能会有 SQL 注入向量。例如;mysqli_real_escape_string 不会保护以下 SQL 注入向量

  $result = "SELECT fields FROM table WHERE id = ".mysqli_real_escape_string($_POST['id']);

如果 $_POST['id'] 包含 23 OR 1=1,则生成的查询将为

  SELECT fields FROM table WHERE id = 23 OR 1=1

这是一个有效的 SQL 注入向量。

(原始函数 mysql_escape_string 没有考虑当前字符集来转义字符串,也没有接受连接参数。自 PHP 4.3.0 起已弃用。)

例如,考虑上面示例之一

$result=mysqli_query($link, 'SELECT * FROM users WHERE username="'.$_GET['username'].'"');

这可以像以下方式进行转义

$result=mysqli_query($link, 'SELECT * FROM users WHERE username="'.mysqli_real_escape_string($_GET['username']).'"');

这样,如果用户试图注入另一个语句,例如 DELETE,它将被无害地解释为 WHERE 子句参数的一部分,如预期的那样

SELECT * FROM `users` WHERE username = '\';DELETE FROM `forum` WHERE title != \''

mysqli_real_escape_string 添加的斜杠使 MySQL 将它们解释为实际的单引号字符,而不是 SQL 语句的一部分。

注意,MySQL 不允许堆叠查询,因此 ;DELETE FROM table 攻击将不起作用

参数化语句

[编辑 | 编辑源代码]

PEAR 的 DB 包[1] 提供了一个 prepare/execute 机制来执行参数化语句。

require_once("DB.php");
$db = &DB::connect("mysql://user:pass@host/database1");
$p = $db->prepare("SELECT * FROM users WHERE username = ?");
$db->execute( $p, array($_GET['username']) );

query() 方法也与 prepare/execute 做同样的事情,

$db->query( "SELECT * FROM users WHERE username = ?", array($_GET['username']) );

prepare/execute 将自动调用 mysql_real_escape_string(),如上一节所述。

在 PHP 版本 5 及更高版本和 MySQL 版本 4.1 及更高版本中,还可以通过 mysqli 扩展[2] 使用预处理语句。示例[3]

$db = new mysqli("localhost", "user", "pass", "database");
$stmt = $db -> prepare("SELECT priv FROM testUsers WHERE username=? AND password=?");
$stmt -> bind_param("ss", $user, $pass);
$stmt -> execute();

类似地,你也可以使用 PHP5[4] 中的内置 PDO 类。

参考资料

[编辑 | 编辑源代码]

更多信息

[编辑 | 编辑源代码]


华夏公益教科书