SQL Injection SQL注入
Abstract
通过不可信来源的输入构建动态 SQL 指令,攻击者就能够修改指令的含义或者执行任意 SQL 命令。
Explanation
SQL injection 错误在以下情况下发生:
- 数据从一个不可信赖的数据源进入程序。
- 数据用于动态地构造一个 SQL 查询。
例1: 以下代码动态地构造并执行了一个SQL查询,该查询可以搜索与指定名称相匹配的项。 该查询仅会显示条目所有者与被授予权限的当前用户一致的条目。
...
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName");
String query = "SELECT * FROM items WHERE owner = '"
+ userName + "' AND itemname = '"
+ itemName + "'";
ResultSet rs = stmt.execute(query);
...
这一代码所执行的查询遵循如下方式:
SELECT * FROM items
WHERE owner = <userName>
AND itemname = <itemName>;
但是,由于这个查询是动态构造的,由一个常数基查询字符串和一个用户输入字符串连接而成,因此只有在itemName 不包含单引号字符时,才会正确执行这一查询。 如果一个用户名为 wiley 的攻击者在
itemName 中输入字符串 "name' OR 'a'='a",那么构造的查询就会变成:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name' OR 'a'='a';
附加条件 OR 'a'='a' 会使 where 从句永远评估为 true,因此该查询在逻辑上将等同于一个更为简化的查询:
SELECT * FROM items;
这种查询的简化会使攻击者绕过查询只返回经过验证的用户所拥有的条目的要求;而现在的查询则会直接返回所有储存在 items 表中的条目,不论它们的所有者是谁。 例 2: 这个例子指出了将不同的恶意数值传递给在例 1 中构造和执行的查询时所带来的各种影响。如果一个用户名为 wiley 的攻击者在 itemName 中输入字符串“name'; DELETE FROM items; --”,那么最后构造的查询将变成两个:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name';
DELETE FROM items;
--'
众多数据库服务器,其中包括 Microsoft(R) SQL Server 2000,都可以一次性执行多条用分号分隔的 SQL 指令。 对于那些不允许运行用分号分隔的批量 SQL 指令的数据库服务器,比如 Oracle 和其他数据库服务器,攻击者输入的这个字符串只会导致错误;但是在那些支持这种操作的数据库服务器上,攻击者可能会通过执行多条 SQL 而在数据库上执行任意 SQL 指令。 注意成对的连字符 (--);这在大多数数据库服务器上都表示下面的语句将作为注释使用,而不能加以执行 [4]。 在这种情况下,注释字符的作用就是删除修改的查询指令中遗留的最后一个单引号。 而在那些不允许这样加注注释的数据库中,通常攻击者可以如例 1 那样来攻击。如果攻击者输入字符串 “name'); DELETE FROM items; SELECT * FROM items WHERE 'a'='a” 就会创建如下三个有效的 SQL 指令:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name';
DELETE FROM items;
SELECT * FROM items WHERE 'a'='a';
避免 SQL injection 攻击的传统方法之一是,把它作为一个输入合法性检查的问题来处理,只接受列在白名单中的字符,或者识别并避免那些列在黑名单中的恶意数据。 白名单方法是一种非常有效方法,它可以强制执行严格的输入检查规则,但是参数化的 SQL 指令所需维护更少,而且能提供更好的安全保障。 而对于通常采用的列黑名单方式,由于总是存在一些小漏洞,所以并不能有效地防止 SQL injection 威胁。 例如,攻击者可以:
— 把没有被黑名单引用的值作为目标
— 寻找方法以绕过对某一转义序列元字符的需要
— 使用存储过程来隐藏注入的元字符 手动去除 SQL 查询中的元字符有一定的帮助,但是并不能完全保护您的应用程序免受SQL injection 攻击。 防范 SQL injection 攻击的另外一种常用方式是使用存储过程。 虽然存储过程可以阻止某些类型的 SQL injection 攻击,但是对于绝大多数攻击仍无能为力。 存储过程有助于避免 SQL injection 的常用方式是限制可作为参数传入的指令类型。 但是,有许多方法都可以绕过这一限制,许多危险的表达式仍可以传入存储过程。 所以再次强调,存储过程可以避免部分情况,但是并不能完全保护您的应用系统抵御SQL injection 的攻击。
Recommendation
造成 SQL injection 攻击的根本原因在于攻击者可以改变 SQL 查询的上下文,使程序员原本要作为数据解析的数值,被篡改为命令了。 当构造一个 SQL 查询时,程序员应当清楚,哪些输入的数据将会成为命令的一部分, 而哪些仅仅是作为数据。 参数化 SQL 指令可以防止直接窜改上下文,避免几乎所有的 SQL injection 攻击。 参数化 SQL 指令是用常规的 SQL 字符串构造的,但是当需要加入用户输入的数据时,它们就需要使用捆绑参数,这些捆绑参数是一些占位符,用来存放随后插入的数据。 换言之,捆绑参数可以使程序员清楚地分辨数据库中的数据,即其中有哪些输入可以看作命令的一部分,哪些输入可以看作数据。 这样,当程序准备执行某个指令时,它可以详细地告知数据库,每一个捆绑参数所使用的运行时的值,而不会被解析成对该命令的修改。 前面的例子可以改成使用参数化 SQL 指令的攻击方式(替代用户输入连续的字符串),如下所示:
...
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName");
String query =
"SELECT * FROM items WHERE itemname=? AND owner=?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, itemName);
stmt.setString(2, userName);
ResultSet results = stmt.execute();
...
更加复杂的情况常常出现在报表生成代码中,因为这时需要通过用户输入来改变 SQL 指令的命令结构,比如在 WHERE 条件子句中加入动态的约束条件。 不要因为这一需求,就无条件地接受连续的用户输入,从而创建查询语句字符串。 当必须要根据用户输入来改变命令结构时,可以使用间接的方法来防止 SQL injection 攻击: 创建一个合法的字符串集合,使其对应于可能要加入到 SQL 指令中的不同元素。 在构造一个 SQL 指令时, 让用户从这个集合中去选择字符串,因为这个集合的字符串在系统的控制之内。