跳到内容

SQL 注入防御速查表

简介

本速查表将帮助您防止应用程序中的 SQL 注入漏洞。它将定义什么是 SQL 注入,解释这些漏洞发生在哪里,并提供四种防御 SQL 注入攻击的选项。SQL 注入攻击很常见,因为

  1. SQL 注入漏洞非常普遍,并且
  2. 应用程序的数据库是攻击者的常见目标,因为它通常包含有趣/关键数据。

什么是 SQL 注入攻击?

如果应用程序具有使用字符串拼接和用户输入进行动态数据库查询的功能,攻击者就可以对其实施 SQL 注入。为避免 SQL 注入漏洞,开发人员需要

  1. 停止编写带有字符串拼接的动态查询,或者
  2. 阻止恶意 SQL 输入被包含在执行的查询中。

有简单的技术可以防止 SQL 注入漏洞,并且这些技术几乎可以与任何编程语言和任何类型的数据库一起使用。虽然 XML 数据库可能存在类似问题(例如 XPath 和 XQuery 注入),但这些技术也可用于保护它们。

典型 SQL 注入漏洞的剖析

Java 中常见的 SQL 注入漏洞如下。由于其未经验证的“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:使用预处理语句(带参数化查询)
  • 选项 2:使用正确构建的存储过程
  • 选项 3:允许列表输入验证
  • 选项 4:强烈不推荐:转义所有用户提供输入

防御选项 1:预处理语句(带参数化查询)

当开发人员学习如何编写数据库查询时,应该被告知使用带有变量绑定(又称参数化查询)的预处理语句。预处理语句编写简单,比动态查询更容易理解,并且参数化查询强制开发人员首先定义所有 SQL 代码,然后将每个参数传递给查询。

如果数据库查询使用这种编码风格,无论用户提供什么输入,数据库都将始终区分代码和数据。此外,预处理语句确保攻击者无法更改查询的意图,即使攻击者插入了 SQL 命令。

安全的 Java 预处理语句示例

在下面的安全 Java 示例中,如果攻击者将用户 ID 输入为 tom' or '1'='1,参数化查询将查找字面上匹配整个字符串 tom' or '1'='1 的用户名。因此,数据库将受到保护,免受恶意 SQL 代码的注入。

以下代码示例使用 PreparedStatement(Java 对参数化查询的实现)来执行相同的数据库查询。

// This should REALLY be validated too
String custname = request.getParameter("customerName");
// Perform input validation to detect attacks
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 预处理语句示例

在 .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 注入)。

Hibernate 查询语言 (HQL) 预处理语句(命名参数)示例

// This 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);

其他安全预处理语句示例

如果您需要预处理查询/参数化语言的示例,包括 Ruby、PHP、Cold Fusion、Perl 和 Rust,请参阅查询参数化速查表或此网站

通常,开发人员喜欢预处理语句,因为所有 SQL 代码都保留在应用程序内部,这使得应用程序相对独立于数据库。

防御选项 2:存储过程

尽管存储过程并非总是能够防止 SQL 注入,但开发人员可以使用某些标准的存储过程编程构造。只要存储过程安全实现(这是大多数存储过程语言的规范),这种方法与使用参数化查询具有相同的效果。

安全的存储过程方法

如果需要存储过程,使用它们最安全的方法要求开发人员构建带有自动参数化参数的 SQL 语句,除非开发人员采取了非常规的操作。预处理语句和存储过程的区别在于,存储过程的 SQL 代码在数据库本身中定义和存储,然后从应用程序中调用。由于预处理语句和安全存储过程在防止 SQL 注入方面同样有效,您的组织应该选择最适合您的方法。

存储过程何时会增加风险

有时,当系统受到攻击时,存储过程可能会增加风险。例如,在 MS SQL Server 上,您有三个主要默认角色:db_datareaderdb_datawriterdb_owner。在使用存储过程之前,DBA 会根据需求授予 Web 服务的用户 db_datareaderdb_datawriter 权限。

然而,存储过程需要执行权限,这是一个默认不提供的角色。在某些用户管理集中但仅限于这 3 个角色的设置中,Web 应用程序必须以 db_owner 身份运行,以便存储过程能够工作。自然,这意味着如果服务器被攻破,攻击者将拥有数据库的完全权限,而在此之前,他们可能只有读取权限。

安全的 Java 存储过程示例

以下代码示例使用 Java 对存储过程接口 (CallableStatement) 的实现来执行相同的数据库查询。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),那么输入验证或查询重新设计是最合适的防御措施。当需要表名或列名时,理想情况下这些值应来自代码而非用户参数。

安全表名验证示例

警告:使用用户参数值来指定表名或列名是设计不佳的症状,如果时间允许,应考虑完全重写。如果无法实现,开发人员应将参数值映射到合法/预期的表名或列名,以确保未经验证的用户输入不会出现在查询中。

在下面的示例中,由于 tableName 被识别为此查询中表名的合法和预期值之一,因此它可以直接附加到 SQL 查询。请记住,如果表名用于未预期的查询中,通用表验证功能可能会导致数据丢失。

String tableName;
switch(PARAM):
  case "Value1": tableName = "fooTable";
                 break;
  case "Value2": tableName = "barTable";
                 break;
  ...
  default      : throw new InputValidationException("unexpected value provided"
                                                  + " for table name");

动态 SQL 生成的最安全用法(不推荐)

当我们说存储过程“安全实现”时,这意味着它不包含任何不安全的动态 SQL 生成。开发人员通常不会在存储过程内部生成动态 SQL。但是,这虽然可以做到,但应该避免。

如果无法避免,存储过程必须使用输入验证或适当的转义,如本文所述,以确保所有提供给存储过程的用户输入都不能用于将 SQL 代码注入到动态生成的查询中。审计人员应始终查找 SQL Server 存储过程中 sp_executeexecuteexec 的使用。对于其他厂商的类似功能,也需要类似的审计指南。

更安全的动态查询生成示例(不推荐)

对于像排序顺序这样简单的事情,最好将用户提供的输入转换为布尔值,然后使用该布尔值选择要附加到查询的安全值。这是动态查询创建中非常标准的需求。

例如:

public String someMethod(boolean sortOrder) {
 String SQLquery = "some SQL ... order by Salary " + (sortOrder ? "ASC" : "DESC");`
 ...

任何时候,如果用户输入在附加到查询之前,或用于选择要附加到查询的值之前,可以转换为非字符串类型(如日期、数字、布尔值、枚举类型等),这就能确保操作的安全性。

在所有情况下,即使使用本文前面讨论的绑定变量,也建议将输入验证作为二级防御措施。有关如何实现强输入验证的更多技术,请参阅输入验证速查表

防御选项 4:强烈不推荐:转义所有用户提供输入

在这种方法中,开发人员会在将所有用户输入放入查询之前对其进行转义。它的实现方式与数据库高度相关。与其他防御措施相比,这种方法很脆弱,我们无法保证此选项在所有情况下都能防止所有 SQL 注入。

如果应用程序是全新构建的或需要较低的风险容忍度,则应使用参数化查询、存储过程或某种对象关系映射器 (ORM) 来构建或重写它,由 ORM 替您构建查询。

额外防御措施

除了采用四种主要防御措施之一外,我们还建议采用所有这些额外的防御措施,以提供纵深防御。这些额外的防御措施是

  • 最小权限原则
  • 允许列表输入验证

最小权限原则

为了最小化成功 SQL 注入攻击的潜在损害,您应该最小化环境中分配给每个数据库账户的权限。从头开始确定您的应用程序账户需要哪些访问权限,而不是试图找出您需要取消哪些访问权限。

确保只需要读取权限的账户只被授予对它们需要访问的表的读取权限。切勿将 DBA 或管理员类型的访问权限分配给您的应用程序账户。我们理解这样做很容易,并且一切似乎都“正常工作”,但这非常危险。

最小化应用程序和操作系统权限

SQL 注入并非数据库数据面临的唯一威胁。攻击者可以简单地将参数值从呈现给他们的合法值更改为对他们未经授权的值,但应用程序本身可能被授权访问。因此,最小化授予应用程序的权限将降低此类未经授权访问尝试的可能性,即使攻击者没有尝试将 SQL 注入作为其攻击的一部分。

与此同时,您还应该最小化 DBMS 运行所用的操作系统账户的权限。不要以 root 或 system 身份运行您的 DBMS!大多数 DBMS 开箱即用时都使用一个非常强大的系统账户。例如,MySQL 在 Windows 上默认以 system 身份运行!将 DBMS 的操作系统账户更改为更合适、权限受限的账户。

开发时的最小权限原则细节

如果账户只需要访问表的部分内容,请考虑创建一个视图来限制对该部分数据的访问,并将账户访问权限分配给该视图,而不是底层表。很少(如果有的话)授予数据库账户创建或删除的权限。

如果您采用一种策略,即到处使用存储过程,并且不允许应用程序账户直接执行自己的查询,那么请将这些账户的权限限制为只能执行它们所需的存储过程。不要直接授予它们对数据库中表的任何权限。

多个数据库的最小管理员权限

Web 应用程序的设计者应避免在 Web 应用程序中使用相同的所有者/管理员账户连接数据库。应为不同的 Web 应用程序使用不同的数据库用户。

一般来说,每个需要访问数据库的独立 Web 应用程序都应该有一个指定的数据库用户账户,供该应用程序连接到数据库。这样,应用程序设计者可以在访问控制方面拥有良好的粒度,从而尽可能地减少权限。每个数据库用户将只拥有其所需的查询访问权限,并根据需要拥有写入权限。

例如,登录页面需要对表中用户名和密码字段的读取访问权限,但不需要任何形式的写入访问权限(无插入、更新或删除)。然而,注册页面肯定需要对该表的插入权限;只有当这些 Web 应用程序使用不同的数据库用户连接数据库时,此限制才能生效。

使用 SQL 视图增强最小权限

您可以使用 SQL 视图通过限制对表特定字段或表连接的读取访问来进一步增加访问的粒度。这可能会带来额外的好处。

例如,如果系统被要求(可能是由于某些特定的法律要求)存储用户的密码,而不是加盐哈希密码,设计者可以使用视图来弥补这一限制。他们可以撤销对表的所有访问(除所有者/管理员之外的所有数据库用户),并创建一个视图,输出密码字段的哈希值而非字段本身。

任何成功的 SQL 注入攻击,如果窃取了数据库信息,也只会限于窃取密码的哈希值(甚至可以是密钥哈希),因为任何 Web 应用程序的数据库用户都无法直接访问表本身。

允许列表输入验证

除了在别无选择时(例如,绑定变量不合法时)作为主要防御措施外,输入验证还可以作为二级防御措施,用于在未经授权的输入传递给 SQL 查询之前对其进行检测。欲了解更多信息,请参阅输入验证速查表。在此处请谨慎操作。通过字符串构建插入到 SQL 查询中的已验证数据不一定安全。

SQL 注入攻击速查表:

以下文章描述了如何在各种平台(本文旨在帮助您避免这些问题)上利用不同类型的 SQL 注入漏洞

SQL 注入漏洞描述:

如何避免 SQL 注入漏洞:

如何审查代码中的 SQL 注入漏洞:

如何测试 SQL 注入漏洞: