1 漏洞描述
SQL 注入攻击(SQL Injection),被广泛用于非法获取网站控制权,是发生在应用程序的数据库层上的安
全漏洞。
在设计不良的程序当中,忽略了对输入字符串中夹带的 SQL 指令的检查,那么这些夹带进去的指令就会
被数据库误认为是正常的 SQL 指令而运行,从而使数据库受到攻击,可能导致数据被窃取、更改、删除,以及进一步导致网站被嵌入恶意代码、被植入后门程序等危害。
XSS 是黑客通过篡改 HTML 代码,来插入并执行恶意脚本的一种攻击。其实,SQL 注入和 XSS 攻击很类似,都是黑客通过篡改代码逻辑发起的攻击。那么,不同的点是什么?SQL 注入到底是什么呢?
通常来说,我们会将应用的用户信息存储在数据库中。每次用户登录时,都会执行一个相应的 SQL 语句。这时,黑客会通过构造一些恶意的输入参数,在应用拼接 SQL 语句的时候,去篡改正常的 SQL 语意,从而执行黑客所控制的 SQL 查询功能。这个过程,就相当于黑客“注入”了一段 SQL 代码到应用中。这就是我们常说的 SQL 注入。
1. 修改 WHERE 语句
我们先来看一个例子。现在有一个简单的登录页面,需要用户输入 Username 和 Password 这两个变量来完成登录。具体的 Web 后台代码如下所示:
uName = getRequestString("username");
uPass = getRequestString("password");
sql = 'SELECT * FROM Users WHERE Username ="' + uName + '" AND Password ="' + uPass + '"'
当用户提交一个表单(假设 Username 为 admin,Password 为 123456)时,Web 将执行下面这行代码:
SELECT * FROM Users WHERE Username ="admin" AND Password ="123456"
用户名密码如果正确的话,这句 SQL 就能够返回对应的用户信息;如果错误的话,不会返回任何信息。因此,只要返回的行数 ≥1,就说明验证通过,用户可以成功登录。
所以,当用户正常地输入自己的用户名和密码时,自然就可以成功登录应用。那黑客想要在不知道密码的情况下登录应用,他又会输入什么呢?他会输入 " or “”="。这时,应用的数据库就会执行下面这行代码:
SELECT * FROM Users WHERE Username ="" AND Password ="" or ""=""
我们可以看到,WHERE 语句后面的判断是通过 or 进行拼接的,其中"“=”"的结果是 true。那么,当有一个 or 是 true 的时候,最终结果就一定是 true 了。因此,这个 WHERE 语句是恒为真的,所以,数据库将返回全部的数据。
这样一来,也就是说,黑客只需要在登录页面中输入 " or “”=",就可以在不知道密码的情况下,成功登录后台了。而这,也就是所谓的“万能密码”。而这个“万能密码”,其实就是通过修改 WHERE 语句,改变数据库的返回结果,实现无密码登录。
2. 执行任意语句
除此之外,大部分的数据库都支持多语句执行。因此,黑客除了修改原本的 WHERE 语句之外,也可以在原语句的后面,插入额外的 SQL 语句,来实现任意的增删改查操作。在实际工作中,MySQL 是最常用的数据库,我们就以它为例,来介绍一下,任意语句是如何执行的。
INSERT INTO Users (Username, Password) VALUES("test","000000"); SELECT * FROM Users;
接下来,我们来看一个具体的例子。在用户完成登录后,应用通常会通过 userId 来获取对应的用户信息。
其 Web 后台的代码如下所示:
uid = getRequestString("userId");
sql = "SELECT * FROM Users WHERE UserId = " + uid;
在这种情况下,黑客只要在传入的 userId 参数中加入一个分号,就可以执行任意的 SQL 语句了。比如,黑客想“删库跑路”的话,就令 userId 为 1;DROP TABLE Users,那么,后台实际执行的 SQL 就会变成下面这行代码,而数据库中所有的用户信息就都会被删除。
SELECT * FROM Users WHERE UserId = 1;DROP TABLE Users
SQL 注入的“姿势”还有很多(比如:没有回显的盲注、基于 INSERT 语句的注入等等),它们的原理都是一样的,都是通过更改 SQL 的语义来执行黑客设定的 SQL 语句。
- 通过 SQL 注入攻击,黑客能做什么?
SQL 注入会令 Web 后台执行非常规的 SQL 语句,从而导致各种各样的问题。那么通过 SQL 注入攻击,黑客究竟能够干些什么呢 ?下面我们就一一来看。
-
绕过验证
在上面的内容中,我们已经介绍过," or “”=" 作为万能密码,可以让黑客在不知道密码的情况下,通过登录认证。因此,SQL 注入最直接的利用方式,就是绕过验证,也就相当于身份认证被破解了。
-
任意篡改数据
除了绕过验证,我们在任意语句执行的部分中讲到,SQL 注入漏洞导致黑客可以执行任意的 SQL 语句。因此,通过插入 DML 类的 SQL 语句( INSERT、UPDATE、DELETE、TRUNCATE、DROP 等),黑客就可以对表数据甚至表结构进行更改,这样数据的完整性就会受到损害。比如上面例子中,黑客通过插入 DROP TABLE Users,删除数据库中全部的用户。
-
窃取数据
在 XSS 漏洞中,黑客可以通过窃取 Cookie 和“钓鱼”获得用户的隐私数据。那么,在 SQL 注入中,黑客会怎么来获取这些隐私数据呢?
在各类安全事件中,我们经常听到“拖库”这个词。所谓“拖库”,就是指黑客通过类似 SQL 注入的手段,获取到数据库中的全部数据(如用户名、密码、手机号等隐私数据)。最简单的,黑客利用 UNION 关键词,将 SQL 语句拼接成下面这行代码之后,就可以直接获取全部的用户信息了。
SELECT * FROM Users WHERE UserId = 1 UNION SELECT * FROM Users
-
消耗资源
SQL 注入破坏可用性十分简单,可以通过完全消耗服务器的资源来实现。比如,在 Web 后台中,黑客可以利用 WHILE 打造死循环操作,或者定义存储过程,触发一个无限迭代等等。在这些情况下,数据库服务器因为 CPU 被迅速打满,持续 100%,而无法及时响应其他请求。
总结来说,通过 SQL 注入攻击,黑客可以绕过验证登录后台,非法篡改数据库中的数据;还能执行任意的 SQL 语句,盗取用户的隐私数据影响公司业务等等。所以,我认为,SQL 注入相当于让黑客直接和服务端的数据库进行了交互。正如我们一直所说的,应用的本质是数据,黑客控制了数据库,也就相当于控制了整个应用。
2. 解决办法
在认识到 SQL 注入的危害之后,我们知道,一个简单的 SQL 查询逻辑,能够带来巨大的安全隐患。因此,我们应该做到在开发过程中就避免出现 SQL 注入漏洞。那具体应该怎么做呢?接下来,我会为你介绍 3 种常见的防护方法,它们分别是:使用 PreparedStatement、使用存储过程和验证输入。接下来,我们一一来看。
1. 使用 PreparedStatement
通过合理地使用 PreparedStatement,我们就能够避免 99.99% 的 SQL 注入问题。你肯定很好奇,我为什么会这么说。接下来,让我们一起看一下它的实现过程。
当数据库在处理一个 SQL 命令的时候,大致可以分为两个步骤:
将 SQL 语句解析成数据库可使用的指令集。我们在使用 EXPLAIN 关键字分析 SQL 语句,就是干的这个事情;
将变量代入指令集,开始实际执行。之所以在批量处理 SQL 的时候能够提升性能,就是因为这样做避免了重复解析 SQL 的过程。
那么 PreparedStatement 为什么能够避免 SQL 注入的问题呢?
这是因为,SQL 注入是在解析的过程中生效的,用户的输入会影响 SQL 解析的结果。因此,我们可以通过使用 PreparedStatement,将 SQL 语句的解析和实际执行过程分开,只在执行的过程中代入用户的操作。这样一来,
无论黑客提交的参数怎么变化,数据库都不会去执行额外的逻辑,也就避免了 SQL 注入的发生。
在 Java 中,我们可以通过执行下面的代码将解析和执行分开:
String sql = "SELECT * FROM Users WHERE UserId = ?";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setInt(1, userId);
ResultSet results = statement.executeQuery();
这里有一点需要你注意,前面我们说了,通过合理地使用 PreparedStatement 就能解决 99.99% 的 SQL 注入问题,那到底怎么做才算“合理地”使用呢?
PreparedStatement 为 SQL 语句的解析和执行提供了不同的“方法”,你需要分开来调用。但是,如果你在使用 PreparedStatement 的时候,还是通过字符串拼接来构造 SQL 语句,那仍然是将解析和执行放在了一块,也就不会产生相应的防护效果了。我这里给你展示了一个错误案例,你可以和上面的代码进行对比。
String sql = "SELECT * FROM Users WHERE UserId = " + userId;
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet results = statement.executeQuery();
2. 使用存储过程
接下来,我们说一说,如何使用存储过程来防止 SQL 注入。实际上,它的原理和使用 PreparedStatement 类似,都是通过将 SQL 语句的解析和执行过程分开,来实现防护。区别在于,存储过程防注入是将解析 SQL 的过程,由数据库驱动转移到了数据库本身。
还是上述的例子,使用存储过程,我们可以这样来实现:
delimiter $$ # 将语句的结束符号从分号 ; 临时改为两个 $$(可以是自定义)
CREATE PROCEDURE select_user(IN p_id INTEGER)
BEGIN
SELECT * FROM Users WHERE UserId = p_id;
END$$
delimiter; # 将语句的结束符号恢复为分号
call select_user(1);
3. 验证输入
防护的核心原则是,一切用户输入皆不可信。因此,SQL 注入的防护手段和 XSS 其实也是相通的,
主要的不同在于:
SQL 注入的攻击发生在输入的时候,因此,我们只能在输入的时候去进行防护和验证;
大部分数据库不提供针对 SQL 的编码,因为那会改变原有的语意,所以 SQL 注入没有编码的保护方案。
因此,对所有输入进行验证或者过滤操作,能够很大程度上避免 SQL 注入的出现。比如,在通过 userId 获取 Users 相关信息的示例中,我们可以确认 userId 必然是一个整数。因此,我们只需要对 userId 参数,进行一个整型转化(比如,Java 中的 Integer.parseInt,PHP 的 intval),就可以实现防护了。
当然,部分场景下,用户输入的参数会比较复杂。我们以用户发出的评论为例,其内容完全由用户定义,应用无法预判它的格式。这种情况下,应用只能通过对部分关键字符进行过滤,来避免 SQL 注入的发生。比如,在 MySQL 中,需要注意的关键词有" % ’ \ _。
这里我简单地总结一下,在实际使用这些防护方法时的注意点。对于验证输入来说,尤其是在复杂场景下的验证输入措施,其防护效果是最弱的。因此,避免 SQL 注入的防护方法,首要选择仍然是 PreparedStatement 或者存储过程。
- 其他解决方案
- 所有的查询语句都使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中。
- 对进入数据库的特殊字符
'"\<>&*;
等进行转义处理,或编码转换。 - 确认每种数据的类型,比如数字型的数据就必须是数字,数据库中的存储字段必须对应为 int 型。
- 数据长度应该严格规定,能在一定程度上防止比较长的 SQL 注入语句无法正确执行。
- 网站每个数据层的编码统一,建议全部使用 UTF-8 编码,上下层编码不一致有可能导致一些过滤模型被绕过。
- 严格限制网站所用数据库账号的权限,给此用户仅提供能够满足其工作的权限,从而最大限度的减少注入攻击对数据库的危害。
- 避免网站显示 SQL 错误信息,比如类型错误、字段不匹配等,防止攻击者利用这些错误信息进行一些判断。
注意:
如果后端开发使用Mybatis ,则需要注意 :
在编写MyBatis的映射语句时,尽量采用“#{xxx}”这样的格式。若不得不使用“${xxx}”这样的参数,要手工
地做好过滤工作,来防止SQL注入攻击。