什么是SQL注入,以及如何在PHP应用程序中进行预防?
你认为你的SQL数据库表现良好,免受即时破坏?然而,SQL注入并不认同!
是的,我们谈论的是即时破坏,因为我不想用“加强安全性”和“防止恶意访问”的老套术语开篇。 SQL Injection是一本书中老掉牙的诡计,每个人,每个开发人员都非常熟悉,也非常清楚如何防止它。除了那一次疏忽大意,结果可能毁灭性的。
如果你已经知道什么是SQL注入,请随时跳到本文后半部分。但对于那些刚刚进入Web开发领域并梦想承担更高级角色的人,需要进行一些介绍。
什么是SQL注入?
理解SQL注入的关键在于它的名字:SQL + 注入。这里的“注入”一词没有任何医学含义,而是使用了动词“注入”。这两个词共同传达了将SQL放入Web应用程序的想法。
把SQL放入Web应用程序…嗯…那不就是我们正在做的事情吗?是的,但我们不希望攻击者控制我们的数据库。让我们通过一个例子来理解这一点。
假设你正在为一个本地电子商务商店构建一个典型的PHP网站,所以你决定添加一个联系表单,像这样:
并且假设文件send_message.php
将所有内容存储在数据库中,以便商店所有者以后可以阅读用户留言。它可能有这样的代码:
<?php
$name = $_POST['name'];
$message = $_POST['message'];
// 检查该用户是否已经有留言
mysqli_query($conn, "SELECT * from messages where name = $name");
// 其他代码在这里
所以你首先要查看这个用户是否已经有未读留言。查询SELECT * from messages where name = $name
看起来很简单,对吗?
错了!
在我们的无辜中,我们为我们的数据库打开了即时破坏的大门。为了实现这一点,攻击者必须满足以下条件:
- 该应用程序正在运行SQL数据库(今天,几乎每个应用程序都是如此)
- 当前数据库连接在数据库上具有“编辑”和“删除”权限
- 重要表的名称可以被猜测
第三点意味着现在攻击者知道你在运行电子商务商店,你非常可能将订单数据存储在orders
表中。凭借这一切,攻击者只需提供这个作为他们的名字:
Joe; truncate orders;
? 是的,先生!让我们看看当PHP脚本执行时查询会变成什么:
SELECT * FROM messages WHERE name = Joe; truncate orders;
好吧,查询的第一部分有语法错误(“Joe”周围没有引号),但分号强制MySQL引擎开始解释一个新的查询:truncate orders
。就这样,在一个单一的攻击中,整个订单历史就消失了!
现在,你知道了SQL注入是如何工作的,是时候看看如何阻止它了。成功进行SQL注入的两个条件是:
- PHP脚本应该对数据库具有修改/删除权限。我认为对于所有应用程序来说,这是正确的,因为你无法使应用程序只读。🙂而且,即使我们删除了所有的修改权限,SQL注入仍然可以允许某人运行SELECT查询并查看所有的数据库,包括敏感数据。换句话说,降低数据库访问级别是无效的,你的应用程序还是需要它。
- 正在处理用户输入。SQL注入可以工作的唯一方法是当你接受用户数据时。再次强调,只因为你担心SQL注入,就停止对应用程序的所有输入是不切实际的。
在PHP中防止SQL注入
现在,考虑到数据库连接、查询和用户输入是生活的一部分,我们如何防止SQL注入呢?幸运的是,这很简单,有两种方法可以做到:1)对用户输入进行清理,2)使用预处理语句。
对用户输入进行清理
如果你正在使用旧版本的PHP(5.5或更低版本,在共享主机上经常发生),最好将所有用户输入通过一个名为mysql_real_escape_string()
的函数运行一遍。基本上,它会从字符串中删除所有特殊字符,使它们在被数据库使用时失去意义。
例如,如果你有一个字符串I'm a string
,单引号字符(‘)可以被攻击者用来操纵正在创建的数据库查询,并导致SQL注入。通过将其通过mysql_real_escape_string()
转换为I'm a string
,它会在单引号前面添加一个反斜杠,对其进行转义。结果,整个字符串现在被作为无害的字符串传递给数据库,而不是能够参与查询操纵。
这种方法有一个缺点:它是一种非常古老的技术,与PHP中旧形式的数据库访问相结合。从PHP 7开始,这个函数甚至都不存在了,这就引出了我们的下一个解决方案。
使用预处理语句
预处理语句是一种使数据库查询更安全可靠的方法。其思想是,我们不将原始查询发送给数据库,而是首先告诉数据库我们将要发送的查询的结构。这就是所谓的“准备”语句。一旦准备好了一个语句,我们就通过参数化输入将信息传递给数据库,以便数据库可以通过将输入插入到之前发送的查询结构中来“填充空白”。这样就消除了输入可能具有的任何特殊能力,使它们在整个过程中被视为仅仅是变量(或有效负载)。以下是预处理语句的示例:
connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// 准备和绑定
$stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
$stmt->bind_param("sss", $firstname, $lastname, $email);
// 设置参数并执行
$firstname = "John";
$lastname = "Doe";
$email = "[email protected]";
$stmt->execute();
$firstname = "Mary";
$lastname = "Moe";
$email = "[email protected]";
$stmt->execute();
$firstname = "Julie";
$lastname = "Dooley";
$email = "[email protected]";
$stmt->execute();
echo "New records created successfully";
$stmt->close();
$conn->close();
?>
我知道,如果你对预处理语句还不熟悉,这个过程听起来可能过于复杂,但是这个概念非常值得努力。这是一个关于它的Here’s很好的介绍。
对于那些已经熟悉PHP的PDO扩展并使用它来创建预处理语句的人,我有一个小建议。
警告:在设置PDO时要小心
在使用PDO进行数据库访问时,我们可能会产生一种错误的安全感。“啊,好吧,我正在使用PDO。现在我不需要考虑其他任何事情了” – 这通常是我们的思维方式。确实,PDO(或MySQLi预处理语句)足以防止各种SQL注入攻击,但在设置时必须小心。通常只是从教程或之前的项目中复制粘贴代码并继续进行,但此设置可能会使一切都无效:
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
这个设置告诉PDO模拟预处理语句而不是实际使用数据库的预处理语句功能。因此,即使您的代码看起来像是创建了预处理语句并设置了参数等等,PHP仍然会向数据库发送简单的查询字符串。换句话说,您与SQL注入的脆弱性还是一样的。🙂
解决方案很简单:确保将此模拟设置为false。
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
现在,PHP脚本被强制在数据库层面上使用预处理语句,从而防止各种SQL注入。
使用WAF进行防止
您知道您还可以使用WAF(Web应用防火墙)保护Web应用程序免受SQL注入的攻击吗?
不仅仅是SQL注入,还有许多其他第7层漏洞,例如跨站脚本,破坏验证,跨站点伪造,数据泄露等。您可以使用自托管的方式,例如Mod Security,也可以使用基于云的方式,如下所示。
SQL注入和现代PHP框架
SQL注入是如此普遍、简单、令人沮丧和危险,以至于所有现代的PHP web frameworks都内置了对策。例如,在WordPress中,我们有$wpdb->prepare()
函数,而如果您正在使用MVC框架,它会为您完成所有繁琐的工作,您甚至不必考虑防止SQL注入。在WordPress中需要明确准备语句有点烦人,但嘿,我们正在谈论WordPress。🙂
无论如何,我的观点是,现代的网络开发人员不必考虑SQL注入,因此他们甚至没有意识到可能性。因此,即使他们在应用程序中留下一个开放的backdoor(可能是一个$_GET查询参数,旧习惯触发了一个不洁的查询),结果可能是灾难性的。因此,深入了解基础知识总是更好的选择。
结论
SQL注入是对Web应用程序的一种非常恶劣的攻击,但可以很容易地避免。正如本文所示,小心处理用户输入(顺便说一下,SQL注入不是处理用户输入带来的唯一威胁),并查询数据库就是全部。也就是说,我们并不总是在一个具有安全性的Web框架中工作,所以最好意识到这种类型的攻击并避免受到它的影响。