什么是 SQL 注入以及如何在 PHP 应用程序中进行预防?

警惕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注入仍然可以允许某人运行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("连接失败: " . $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框架的安全性,因此最好注意此类攻击,不要上当受骗。