警惕SQL注入:你的数据库真的安全吗?
你或许认为你的SQL数据库运行良好且坚不可摧?但SQL注入可不这么认为!
没错,我们讨论的是即时破坏,而非那些“增强安全性”和“防止恶意访问”等陈词滥调。SQL注入是个老生常谈的问题,每个开发者都应该非常熟悉,也清楚如何防范。但偶尔的疏忽,便可能导致灾难性的后果。
如果你已经了解SQL注入,可以直接跳到本文的后半部分。但对于刚踏入Web开发领域,渴望成为资深开发者的朋友,一些基础知识的介绍是必不可少的。
什么是SQL注入?
理解SQL注入的关键在于它的名称:SQL + 注入。“注入”在此并非医学概念,而是动词“注入”的用法。这两个词组合在一起,表示将SQL代码插入Web应用程序的行为。
将SQL代码插入Web应用程序……这不正是我们在做的事情吗?是的,但我们不希望数据库被攻击者控制。让我们通过一个示例来理解这一点。
假设你正在为一家本地电商商店搭建一个典型的PHP网站,你决定添加一个联系表单,代码如下:
假设send_message.php
文件会将所有内容存储在数据库中,以便店主稍后读取用户消息。代码可能如下:
所以,你首先尝试检查此用户是否已经有未读消息。查询语句SELECT * from messages where name = $name
看起来很简单,对吧?
大错特错!
我们天真地打开了数据库即时被破坏的大门。为此,攻击者需要满足以下条件:
- 应用程序运行在SQL数据库上(现在几乎所有应用程序都如此)
- 当前的数据库连接具有对数据库的“编辑”和“删除”权限
- 重要的表名可以被猜到
第三点意味着,如果攻击者知道你经营一家电商商店,那么你很可能将订单数据存储在orders
表中。有了这些信息,攻击者只需将以下内容作为他们的名字提交:
乔; TRUNCATE orders; 是,先生!让我们看看,当PHP脚本执行这个查询时,它会变成什么:
SELECT * FROM messages WHERE name = Joe; TRUNCATE orders;
是的,查询的第一部分存在语法错误(“Joe”周围缺少引号),但分号强制MySQL引擎开始解释新的部分:TRUNCATE orders
。就这样,整个订单记录消失得无影无踪!
现在你已经了解了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("连接失败: " . $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 "新记录创建成功"; $stmt->close(); $conn->close(); ?>
我知道如果你不熟悉预处理语句,这个过程听起来会不必要地复杂,但这个概念非常值得我们付出努力。 这里 有一个很好的介绍。
对于那些已经熟悉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框架都内置了应对措施。例如,在WordPress中,我们有$wpdb->prepare()
函数,而如果你使用的是MVC框架,它会为你完成所有繁琐的工作,你甚至不必考虑防止SQL注入。在WordPress中,你必须显式地准备语句,这有点麻烦,但是,嘿,我们讨论的是WordPress。🙂
无论如何,我的意思是,现代的Web开发者不必考虑SQL注入,因此,他们甚至可能意识不到它的存在。因此,即使他们在应用程序中留下后门(可能是$_GET
查询参数和开始脏查询的旧习惯),结果也可能是灾难性的。因此,最好花时间深入研究基础知识。
总结
SQL注入对Web应用程序是一种非常危险的攻击,但很容易避免。正如我们在本文中看到的,在处理用户输入时要小心(顺便说一下,SQL注入不是处理用户输入带来的唯一威胁),并且查询数据库是重中之重。也就是说,我们并不总是依赖于Web框架的安全性,因此最好注意此类攻击,不要上当受骗。