Overview
调用通过不可信赖的数据源输入构建的 SQL 查询 mysql_query()。通过这种调用,攻击者能够修改语句的含义或执行任意 SQL 命令。
Details
SQL injection 错误在以下情况下发生:
-
数据从一个不可信赖的数据源进入程序。
-
数据用于动态地构造一个 SQL 查询。 这种情况下,数据被传递给代码中的 mysql_query()。
**例 1:**以下代码动态地构造并执行了一个 SQL 查询,该查询可以搜索与指定名称相匹配的项。该查询仅会显示条目所有者与被授予权限的当前用户一致的条目。
$userName = $_SESSION['userName']; $itemName = $_POST['itemName'];
query = "SELECT \* FROM items WHERE owner = 'userName' AND itemname = '$itemName';";
result = mysql_query(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:**此示例说明了将不同的恶意值传递给Example 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 指令。对于那些不允许运行用分号分隔的批量指令的数据库服务器,比如 Oracle 和其他数据库服务器,攻击者输入的这个字符串只会导致错误;但是在那些支持这种操作的数据库服务器上,攻击者可能会通过执行多条指令而在数据库上执行任意命令。 注意末尾的一对连字符 (--);这在大多数数据库服务器上都表示该语句剩余部分将视为注释,不会加以执行 [4]。在这种情况下,可通过注释字符删除修改后的查询遗留的末尾单引号。而在不允许通过这种方式使用注释的数据库上,攻击者通常仍可使用类似于Example 1.中所用的技巧进行攻击。
如果攻击者输入字符串"name'); DELETE FROM items; SELECT * FROM items WHERE 'a'='a",将创建以下三个有效语句:
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 的攻击。
Recommendations
造成 SQL injection 攻击的根本原因在于攻击者可以改变 SQL 查询的上下文,使程序员原本要作为数据解析的数值,被篡改为命令了。当构造一个 SQL 查询时,程序员应当清楚,哪些输入的数据将会成为命令的一部分,而哪些仅仅是作为数据。参数化 SQL 指令可以防止直接窜改上下文,避免几乎所有的 SQL injection 攻击。参数化 SQL 指令是用常规的 SQL 字符串构造的,但是当需要加入用户输入的数据时,它们就需要使用捆绑参数,这些捆绑参数是一些占位符,用来存放随后插入的数据。换言之,捆绑参数可以使程序员清楚地分辨数据库中的数据,即其中有哪些输入可以看作命令的一部分,哪些输入可以看作数据。这样,当程序准备执行某个指令时,它可以详细地告知数据库,每一个捆绑参数所使用的运行时的值,而不会被解析成对该命令的修改。 当连接到 MySQL 时,可以将前面的例子改写为使用参数化 SQL 指令(而不是联结用户输入的字符串),如下所示:
mysqli = new mysqli(host,$dbuser, $dbpass, $db);
$userName = $_SESSION['userName'];
$itemName = $_POST['itemName'];
$query = "SELECT * FROM items WHERE owner = ? AND itemname = ?";
$stmt = mysqli-\>prepare(query);
stmt-\>bind_param('ss',username,$itemName);
$stmt->execute();
MySQL Improved 扩展 (mysqli) 适用于 MySQL 中的 PHP5 用户。依赖于不同数据库的代码应该检查相似的扩展名。 更加复杂的情况常常出现在报表生成代码中,因为这时需要通过用户输入来改变 SQL 指令的命令结构,比如在 WHERE 条件子句中加入动态的约束条件。不要因为这一需求,就无条件地接受连续的用户输入,从而创建查询语句字符串。当必须要根据用户输入来改变命令结构时,可以使用间接的方法来防止 SQL injection 攻击:创建一个合法的字符串集合,使其对应于可能要加入到 SQL 指令中的不同元素。在构造一个 SQL 指令时,可使用来自用户的输入,以便从应用程序控制的值集合中进行选择。
个人认为最简单的方法是直接过滤掉SQL关键字,以及=,单引号等。