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