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]。
- ↑ http://pear.php.net/package/DB
- ↑ Mysqli 扩展的官方文档,php.net。
- ↑ PHP 和 MySQLi 中的预备语句,Matt Bango。
- ↑ https://php.ac.cn/manual/en/book.pdo.php