SQL 注入如何预防?
本文参考自 owasp,重点是提供清晰,简单,可操作的指导,以防止应用程序中的 SQL 注入漏洞。不幸的是,SQL 注入攻击很常见,这是由于两个因素:
SQL 注入漏洞的显着流行
目标的吸引力(即数据库通常包含应用程序的所有有趣/关键数据)。
发生了如此多的成功 SQL 注入攻击有点可耻,因为在代码中避免 SQL 注入漏洞非常简单。
当软件开发人员创建包含用户提供的输入的动态数据库查询时,会引入 SQL 注入漏洞。为了避免 SQL 注入缺陷很简单。开发人员需要:
a)停止编写动态查询;
b)防止用户提供的包含恶意 SQL 的输入影响所执行查询的逻辑。
本文提供了一组通过避免这两个问题来防止 SQL 注入漏洞的简单技术。这些技术几乎可以与任何类型的数据库一起使用。还有其他类型的数据库,如 XML 数据库,可能有类似的问题(例如,XPath 和 XQuery 注入),这些技术也可用于保护它们。
主要防御:
选项 1:使用准备好的语句(带参数化查询)
选项 2:使用存储过程
选项 3:白名单输入验证
选项 4:转义所有用户提供的输入
额外防御:
另外:强制执行最低权限
另外:执行白名单输入验证作为辅助防御
不安全的例子:
SQL 注入漏洞通常如下所示:
以下(Java)示例是 UNSAFE,并允许攻击者将代码注入将由数据库执行的查询中。简单地附加到查询的未经验证的“customerName”参数允许攻击者注入他们想要的任何 SQL 代码。不幸的是,这种访问数据库的方法太常见了。
String query = "SELECT account_balance FROM user_data WHERE user_name = "
+ request.getParameter("customerName");
try {
Statement statement = connection.createStatement( ... );
ResultSet results = statement.executeQuery( query );
}
...
主要防御
防御选项 1:准备好的语句(带参数化查询)#
使用带有变量绑定的预准备语句(也就是参数化查询)是所有开发人员应该首先学习如何编写数据库查询的方法。它们比动态查询更易于编写,更易于理解。参数化查询强制开发人员首先定义所有 SQL 代码,然后将每个参数传递给查询。这种编码风格允许数据库区分代码和数据,无论提供什么用户输入。
准备好的语句可确保攻击者无法更改查询的意图,即使攻击者插入了 SQL 命令也是如此。在下面的安全示例中,如果攻击者输入的是 userID tom' or '1'='1,则参数化查询不会受到攻击,而是会查找与字符串完全匹配的用户名 tom' or '1'='1。
特定语言的建议:
Java EE - PreparedStatement()与绑定变量一起使用
.NET - 使用参数化查询,如绑定变量 SqlCommand()或 OleDbCommand()使用绑定变量
PHP - 将 PDO 与强类型参数化查询一起使用(使用 bindParam())
Hibernate - createQuery()与绑定变量一起使用(在 Hibernate 中称为命名参数)
SQLite - 用于 sqlite3_prepare()创建语句对象
在极少数情况下,准备好的陈述会损害绩效。遇到这种情况时,最好是 a)强烈验证所有数据或 b)使用特定于数据库供应商的转义例程来转义所有用户提供的输入,如下所述,而不是使用预准备语句。
安全 JavaSQL 语句示例
以下代码示例使用 PreparedStatementJava 的参数化查询实现来执行相同的数据库查询。
// 一定要验证
String custname = request.getParameter("customerName");
String query = "SELECT account_balance FROM user_data WHERE user_name = ? ";
PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, custname);
ResultSet results = pstmt.executeQuery( );
安全 C#.NET SQL 语句示例
使用.NET,它更加直接。查询的创建和执行不会更改。您所要做的就是使用 Parameters.Add()此处所示的调用将参数传递给查询。
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
try {
OleDbCommand command = new OleDbCommand(query, connection);
command.Parameters.Add(new OleDbParameter("customerName", CustomerName Name.Text));
OleDbDataReader reader = command.ExecuteReader();
// …
} catch (OleDbException se) {
// error handling
}
我们已经在 Java 和.NET 中展示了示例,但实际上所有其他语言(包括 Cold Fusion 和 Classic ASP)都支持参数化查询接口。甚至 SQL 抽象层,如 Hibernate 查询语言(HQL)也有相同类型的注入问题(我们称之为 HQL 注入)。HQL 也支持参数化查询,因此我们可以避免这个问题:
Hibernate 查询语言(HQL)准备语句(命名参数)示例
//First is an unsafe HQL Statement
Query unsafeHQLQuery = session.createQuery("from Inventory where productID='"+userSuppliedParameter+"'");
//Here is a safe version of the same query using named parameters
Query safeHQLQuery = session.createQuery("from Inventory where productID=:productid");
safeHQLQuery.setParameter("productid", userSuppliedParameter);
开发人员倾向于喜欢 Prepared Statement 方法,因为所有 SQL 代码都保留在应用程序中。这使您的应用程序相对数据库独立。
防御选项 2:存储过程 #
SQL 注入并不总是安全的存储过程。但是,某些标准存储过程编程结构与安全实现时使用参数化查询具有相同的效果,这是大多数存储过程语言的标准。
它们要求开发人员只使用自动参数化的参数构建 SQL 语句,除非开发人员在很大程度上超出了标准。预准备语句和存储过程之间的区别在于,存储过程的 SQL 代码已定义并存储在数据库本身中,然后从应用程序中调用。这两种技术在防止 SQL 注入方面具有相同的效果,因此您的组织应该选择哪种方法对您最有意义。
注意:'安全实现'意味着存储过程不包含任何不安全的动态 SQL 生成。开发人员通常不会在存储过程中生成动态 SQL。但是,它可以做到,但应该避免。如果无法避免,则存储过程必须使用输入验证或本文所述的正确转义,以确保不能使用所有用户提供的存储过程输入将 SQL 代码注入动态生成的查询中。审计人员应始终在 SQL Server 存储过程中查找 sp_execute,execute 或 exec 的用法。类似的审计指南对于其他供应商的类似功能是必要的。
在某些情况下,存储过程可能会增加风险。例如,MS SQL 服务器上,你有 3 个主要的默认角色:db_datareader,db_datawriter 和 db_owner。在存储过程开始使用之前,DBA 会根据要求为 webservice 的用户提供 db_datareader 或 db_datawriter 权限。但是,存储过程需要执行权限,默认情况下该角色不可用。用户管理已集中在一些设置,但仅限于这 3 个角色,导致所有 Web 应用程序在 db_owner 权限下运行,因此存储过程可以正常工作。当然,这意味着如果服务器遭到破坏,攻击者拥有数据库的完全权限,以前他们可能只具有读访问权限。
安全 Java 存储过程示例
以下代码示例使用 CallableStatementJava 的存储过程接口实现来执行相同的数据库查询。该 sp_getAccountBalance 存储过程将在数据库中被预先定义和执行相同的功能与上述定义的查询。
// This should REALLY be validated
String custname = request.getParameter("customerName");
try {
CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
cs.setString(1, custname);
ResultSet results = cs.executeQuery();
// … result set handling
} catch (SQLException se) {
// … logging and error handling
}
安全的 VB .NET 存储过程示例
以下代码示例使用 SqlCommand.NET 的存储过程接口实现来执行相同的数据库查询。该 sp_getAccountBalance 存储过程将在数据库中被预先定义和执行相同的功能与上述定义的查询。
Try
Dim command As SqlCommand = new SqlCommand("sp_getAccountBalance", connection)
command.CommandType = CommandType.StoredProcedure
command.Parameters.Add(new SqlParameter("@CustomerName", CustomerName.Text))
Dim reader As SqlDataReader = command.ExecuteReader()
'...
Catch se As SqlException
'error handling
End Try
防御选项 3:白名单输入验证 #
SQL 查询的各个部分不是使用绑定变量的合法位置,例如表或列的名称,以及排序顺序指示符(ASC 或 DESC)。在这种情况下,输入验证或查询重新设计是最合适的防御。对于表或列的名称,理想情况下,这些值来自代码,而不是来自用户参数。
但是,如果用户参数值用于使表名和列名不同,则应将参数值映射到合法/预期的表或列名,以确保未经验证的用户输入不会在查询中结束。请注意,这是设计不佳的症状,如果时间允许,应考虑完全重写。
以下是表名验证的示例。
String tableName;
switch(PARAM):
case "Value1": tableName = "fooTable";
break;
case "Value2": tableName = "barTable";
break;
...
default : throw new InputValidationException("unexpected value provided"
+ " for table name");
该 tableName 然后可以直接附加到 SQL 查询,因为它是目前已知的是在此查询表名的法律和预期值之一。请记住,通用表验证功能可能会导致数据丢失,因为表名用于不期望它们的查询中。
对于像排序顺序这样简单的东西,最好将用户提供的输入转换为布尔值,然后使用该布尔值选择要附加到查询的安全值。这是动态查询创建中非常标准的需求。
例如:
public String someMethod(boolean sortOrder) {
String SQLquery = "some SQL ... order by Salary " + (sortOrder ? "ASC" : "DESC");`
...
任何时候用户输入都可以转换为非 String,如日期,数字,布尔值,枚举类型等,然后将其附加到查询中,或用于选择要追加到查询的值,这可以确保它是这样做是安全的。
在所有情况下,也建议将输入验证作为辅助防御,即使使用绑定变量,如本文后面所述。有关如何实施强白名单输入验证的更多技术在输入验证备忘单中进行了描述。
防御选项 4:转义所有用户提供的输入 #
当上述任何一种方法都不可行时,该技术仅应作为最后的手段使用。输入验证可能是一个更好的选择,因为与其他防御相比,这种方法很脆弱,我们不能保证它会在所有情况下阻止所有 SQL 注入。
此技术是在将用户输入放入查询之前将其转义。它的实现在数据库方面非常具体。通常只建议在实现输入验证时不会降低遗留代码的成本效益。应该使用参数化查询,存储过程或某种为您构建查询的对象关系映射器(ORM)来构建或重写从头开始构建的应用程序或需要低风险容忍度的应用程序。
这种技术就是这样的。每个 DBMS 都支持一种或多种特定于某些查询的字符转义方案。如果您使用正在使用的数据库的正确转义方案转义所有用户提供的输入,则 DBMS 不会将该输入与开发人员编写的 SQL 代码混淆,从而避免任何可能的 SQL 注入漏洞。
要专门为数据库编码器查找 javadoc,请单击 Codec 左侧的类。有很多编解码器实现。两个特定于数据库的编解码器是 OracleCodec,和 MySQLCodec。
只需 All Known Implementing Classes:在 Interface Codec 页面顶部单击其名称即可。
目前,ESAPI 目前拥有以下数据库编码器:
MySQL(支持 ANSI 和本机模式)
数据库编码器即将推出:
SQL Server
PostgreSQL 的
如果您的数据库编码器丢失,请告诉我们。
特定于数据库的转义详细信息 #
如果您想构建自己的转义例程,以下是我们为 ESAPI 编码器开发的每个数据库的转义细节:
SQL Server
DB2
转义动态查询
使用 ESAPI 数据库编解码器非常简单。Oracle 示例如下所示:
ESAPI.encoder().encodeForSQL( new OracleCodec(), queryparam );
因此,如果您在代码中生成了一个现有的动态查询,该查询将转到 Oracle,如下所示:
String query = "SELECT user_id FROM user_data WHERE user_name = '"
+ req.getParameter("userID")
+ "' and user_password = '" + req.getParameter("pwd") +"'";
try {
Statement statement = connection.createStatement( … );
ResultSet results = statement.executeQuery( query );
}
你会重写第一行看起来像这样:
Codec ORACLE_CODEC = new OracleCodec();
String query = "SELECT user_id FROM user_data WHERE user_name = '"
+ ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("userID"))
+ "' and user_password = '"
+ ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("pwd")) +"'";
无论输入是什么,它现在都可以安全地进行 SQL 注入。
为了获得最大的代码可读性,您还可以构建自己的代码 OracleEncoder:
Encoder oe = new OracleEncoder();
String query = "SELECT user_id FROM user_data WHERE user_name = '"
+ oe.encode( req.getParameter("userID")) + "' and user_password = '"
+ oe.encode( req.getParameter("pwd")) +"'";
使用这种类型的解决方案,您只需要将每个用户提供的参数包装成一个 ESAPI.encoder().encodeForOracle( )调用或者您命名为调用的任何内容,您就可以完成。
在 Like 子句中转义通配符
该 LIKE 关键字允许进行文本扫描搜索。在 Oracle 中,下划线_字符仅匹配一个字符,而&符号 %用于匹配任何字符的零次或多次出现。必须在 LIKE 子句条件中转义这些字符。
例如:
SELECT name FROM emp WHERE id LIKE '%/_%' ESCAPE '/';
SELECT name FROM emp WHERE id LIKE '%\%%' ESCAPE '\';
MySQL 转义 #
MySQL 支持两种转义模式:
ANSI_QUOTES SQL 模式,以及这个关闭的模式,我们称之为
MySQL 模式。
ANSI SQL 模式:'使用''(两个单一刻度)简单编码所有(单个刻度)字符
MySQL 模式,执行以下操作:
NUL (0x00) --> \0 [This is a zero, not the letter O]
BS (0x08) --> \b
TAB (0x09) --> \t
LF (0x0a) --> \n
CR (0x0d) --> \r
SUB (0x1a) --> \Z
" (0x22) --> \"
% (0x25) --> \%
' (0x27) --> \'
\ (0x5c) --> \\
_ (0x5f) --> \_
all other non-alphanumeric characters with ASCII values
less than 256 --> \c where 'c' is the original non-alphanumeric character.
SQL Server 转义 #
我们还没有实现 SQL Server 转义例程,但是下面有很好的指针和链接到描述如何防止 SQL 服务器上的 SQL 注入攻击的文章,请参见此处。
DB2 转义 #
此信息基于 DB2 WebQuery 特殊字符以及 Oracle JDBC DB2 驱动程序中的一些信息。
有关几个 DB2 Universal 驱动程序之间差异的信息。
十六进制编码所有输入 #
转义的一个特殊情况是对从用户接收的整个字符串进行十六进制编码的过程(这可以看作是转义每个字符)。Web 应用程序应在将用户输入包含在 SQL 语句中之前对其进行十六进制编码。SQL 语句应该考虑到这一事实,并相应地比较数据。
例如,如果我们必须查找匹配 sessionID 的记录,并且用户将字符串 abc123 作为会话 ID 发送,则 select 语句将为:
SELECT ... FROM session WHERE hex_encode(sessionID) = '616263313233'
hex_encode 应该由所使用的数据库的特定工具替换。字符串 606162313233 是从用户接收的字符串的十六进制编码版本(它是用户数据的 ASCII / UTF-8 代码的十六进制值的序列)。
如果攻击者要传输包含单引号字符的字符串,然后尝试注入 SQL 代码,则构造的 SQL 语句将只显示如下:
... WHERE hex_encode ( ... ) = '2720 ... '
27 是单引号的 ASCII 代码(十六进制),它与字符串中的任何其他字符一样只是十六进制编码。产生的 SQL 只能包含数字数字和字母 a 来 f,从来没有任何特殊字符,它可能会使 SQL 注入。
在 PHP 中转义 SQLi#
使用预准备语句和参数化查询。这些是由数据库服务器与任何参数分开发送和解析的 SQL 语句。这样攻击者就无法注入恶意 SQL。
你基本上有两个选择来实现这个目标:
使用 PDO(适用于任何支持的数据库驱动程序):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute(array('name' => $name));
foreach ($stmt as $row) {
// do something with $row
}
使用 MySQLi(用于 MySQL):
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// do something with $row
}
PDO 是通用选项。如果您要连接到 MySQL 以外的数据库,则可以引用特定于驱动程序的第二个选项(例如,对于 PostgreSQL,请使用 pg_prepare()和 pg_execute())。
额外的防御
除了采用四种主要防御之一外,我们还建议采用所有这些额外的防御措施,以便提供深度防御。这些额外的防御是:
最低权限
白名单输入验证
最低权限 #
为了最大限度地减少成功 SQL 注入攻击的潜在损害,您应该最小化分配给环境中每个数据库帐户的权限。不要为您的应用程序帐户分配 DBA 或管理员类型访问权限。我们知道这很容易,当你这样做时,一切都“有效”,但这是非常危险的。
从头开始确定您的应用程序帐户需要哪些访问权限,而不是试图找出您需要带走的访问权限。确保仅需要读访问权限的帐户才被授予对他们需要访问的表的读访问权限。
如果帐户只需要访问表的某些部分,请考虑创建一个视图,以限制对该部分数据的访问,并为帐户分配帐户访问权限,而不是基础表。很少,如果有的话,授予对数据库帐户的创建或删除访问权限。
如果您采用的策略是在任何地方使用存储过程,并且不允许应用程序帐户直接执行自己的查询,那么将这些帐户限制为只能执行所需的存储过程。不要直接向数据库中的表授予任何权限。
SQL 注入不是对数据库数据的唯一威胁。攻击者可以简单地将参数值从它们所呈现的合法值之一更改为未经授权的值,但应用程序本身可能被授权访问。因此,尽量减少授予应用程序的权限将降低此类未经授权的访问尝试的可能性,即使攻击者没有尝试将 SQL 注入用作其漏洞利用的一部分。
在您使用它时,您应该最小化 DBMS 运行的操作系统帐户的权限。不要以 root 用户身份或系统运行 DBMS!大多数 DBMS 都是开箱即用的,具有非常强大的系统帐户。例如,默认情况下,MySQL 在 Windows 上作为系统运行!使用受限制的权限将 DBMS 的 OS 帐户更改为更合适的帐户。
多个 DB 用户 #
Web 应用程序的设计者不仅应避免在 Web 应用程序中使用相同的所有者/管理员帐户来连接到数据库。不同的 DB 用户可以用于不同的 Web 应用程序。
通常,需要访问数据库的每个单独的 Web 应用程序都可以具有指定的数据库用户帐户,Web 应用程序将使用该帐户连接到数据库。这样,应用程序的设计者可以在访问控制中具有良好的粒度,从而尽可能地减少特权。然后,每个数据库用户都可以选择访问它所需的内容,并根据需要进行写访问。
例如,登录页面需要对表的用户名和密码字段进行读访问,但不能对任何表单进行写访问(无插入,更新或删除)。但是,注册页面当然需要对该表的插入权限; 只有当这些 Web 应用程序使用不同的 DB 用户连接到数据库时,才能强制执行此限制。
查看 #
通过限制对表的特定字段或表的连接的读访问,可以使用 SQL 视图进一步增加访问的粒度。它可能具有额外的好处:例如,假设系统需要(可能由于某些特定的法律要求)来存储用户的密码,而不是盐渍的密码。
设计师可以使用视图来弥补这种限制; 撤消对表的所有访问(来自除所有者/管理员之外的所有数据库用户)并创建一个输出密码字段的哈希而不是字段本身的视图。任何成功窃取数据库信息的 SQL 注入攻击都将被限制为窃取密码的哈希值(甚至可能是键控哈希值),因为任何 Web 应用程序的数据库用户都无权访问表本身。
评论