PHP编程
SQL 注入攻击
问题
考虑以下 PHP 中的 SQL 查询:
$result = mysql_query('SELECT * FROM users WHERE username="' . $_GET['username'] . '"');
该查询从 users
表中选择所有行,其中 username
等于查询字符串中的值。如果仔细观察,您会发现该语句容易受到 SQL 注入攻击——因为 $_GET['username']
中的引号没有被转义,因此它会作为语句的一部分连接到查询中,这可能允许恶意行为。
假设 $_GET['username']
的值是以下内容:" OR 1 OR username = "
(一个双引号,后跟文本 " OR 1 OR username = "
,再跟另一个双引号)。将其连接到原始表达式后,查询变成了如下形式:
SELECT * FROM users WHERE username = "" OR 1 OR username = ""
通过这种方式,SQL 语句看起来是冗余的 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 {
// 不查询数据库
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($link, $_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 包提供了一个准备/执行机制来执行参数化语句。
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]。
参考文献
- PEAR DB 包
- Mysqli 扩展的官方文档,php.net。
- PHP 和 MySQLi 中的预处理语句
- PDO 文档
更多信息