跳转到内容

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 连接, ]字符串 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 中的内置 PDO 类[4]

参考文献

[编辑 | 编辑源代码]

更多信息

[编辑 | 编辑源代码]


华夏公益教科书