如果您要对 SQL 数据库使用窗体身份验证,则应采取本节介绍的预防措施来防范 SQL 注入式攻击。SQL 注入式攻击是将额外的(恶意的)SQL 代码传递到某个应用程序中的行为,该代码通常被附加到该应用程序内包含的合法 SQL 代码中。所有 SQL 数据库都容易受到不同程度的 SQL 注入式攻击,但本章的侧重点是 SQL Server 数据库。
当您处理作为 SQL 命令组成部分的用户输入时,应特别注意可能发生的 SQL 注入式攻击。如果您的身份验证方案用于为 SQL 数据库验证用户身份(例如您对 SQL Server 使用窗体身份验证),则必须防范 SQL 注入式攻击。
如果您使用未筛选的输入来生成 SQL 字符串,则应用程序可能会遭受恶意用户输入(注意,切勿信任用户输入)的攻击。其中的危险在于,当您将用户输入插入一个将要成为可执行语句的字符串中时,恶意用户可以利用转义符将 SQL 命令附加到您想用的 SQL 语句中。
下面几节中的代码片段使用 SQL Server 附带的 Pubs 数据库来说明 SQL 注入式攻击的示例。
问题
当您将用户输入或其他未知数据添加到数据库查询中时,您的应用程序会很容易遭受 SQL 注入式攻击。例如,下面的两个代码片段都易于受到攻击。
| • |
您用未筛选的用户输入生成 SQL 语句。 SqlDataAdapter myCommand = new SqlDataAdapter(
"SELECT au_lname, au_fname FROM authors WHERE au_id = '" +
Login.Text + "'", myConnection);
|
| • |
您通过生成包含未筛选的用户输入的单个字符串来调用存储过程。 SqlDataAdapter myCommand = new SqlDataAdapter("LoginStoredProcedure '" +
Login.Text + "'", myConnection);
|
SQL 脚本注入式攻击的剖析
当您在应用程序中接受未筛选的用户输入值(如上所示)时,恶意用户可以使用转义符来追加自己的命令。
请看一个 SQL 查询,在该查询中,用户的输入是社会保险号码(如 172–32–xxxx)形式,得到的查询如下所示:
SELECT au_lname, au_fname FROM authors WHERE au_id = '172-32-xxxx'
恶意用户可以将以下文字输入您的应用程序输入字段(例如文本框控件)中:
' ; INSERT INTO jobs (job_desc, min_lvl, max_lvl) VALUES ('Important Job', 25,
100) -
此示例中注入了一个 INSERT 语句(但是允许用于连接到 SQL Server 的帐户使用的任何语句都可以执行)。如果该帐户是 sysadmin 角色的成员(它允许执行使用 xp_cmdshell 的 shell 命令),并且 SQL Server 使用的域帐户拥有对其他网络资源的访问权限,则该代码的破坏性特别大。
上述命令生成以下组合的 SQL 字符串:
SELECT au_lname, au_fname FROM authors WHERE au_id = '';INSERT INTO jobs
(job_desc, min_lvl, max_lvl) VALUES ('Important Job', 25, 100) --
在这种情况下,恶意输入开头的 '(单引号)字符成为您的 SQL 语句中的当前字符串的结束字符。它仅在以下情况下才结束当前语句:下面被分析的标记不作为当前语句的继续标记,而是作为一个新语句的开始标记。
SELECT au_lname, au_fname FROM authors WHERE au_id = ' '
;(分号)字符告诉 SQL 您正在开始一个新语句,而紧跟在后面的就是恶意 SQL 代码:
; INSERT INTO jobs (job_desc, min_lvl, max_lvl) VALUES ('Important Job', 25, 100)
注意:分隔 SQL 语句不一定需要使用分号。这与供应商/实现方法有关,但 SQL Server 不需要。例如,SQL Server 将以下语句分析为两个独立的语句:
SELECT * FROM MyTable DELETE FROM MyTable
最后,––(双短划线)字符序列是一个 SQL 注释符号,它告诉 SQL 忽略文本其余部分,在此示例中就是忽略 '(单引号)这个结束字符(否则,会导致 SQL 分析器错误)。
作为上述语句的结果,SQL 执行的完整文本是:
SELECT au_lname, au_fname FROM authors WHERE au_id = '' ; INSERT INTO jobs
(job_desc, min_lvl, max_lvl) VALUES ('Important Job', 25, 100) --'
解决方案
以下几种方法可用于从应用程序安全调用 SQL:
| • |
在生成 SQL 语句时使用 Parameters 集合。 SqlDataAdapter myCommand = new SqlDataAdapter(
"SELECT au_lname, au_fname FROM Authors WHERE au_id= @au_id",
myConnection);
SqlParameter parm = myCommand.SelectCommand.Parameters.Add(
"@au_id",
SqlDbType.VarChar, 11);
parm.Value= Login.Text;
|
| • |
在调用存储过程时使用 Parameters 集合。 // AuthorLogin 是一个存储过程,它接受名称为 Login 的参数
SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", myConnection);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add(
"@LoginId", SqlDbType.VarChar,11);
parm.Value=Login.Text;
如果您使用 Parameters 集合,则不管恶意用户在输入中包含什么内容,该输入都会当作文本进行处理。使用 Parameters 集合的另一个优势是您可以执行类型和长度检查。超出范围的值会触发异常。这是一个有效的深层防御示例。 |
| • |
从用户输入中筛选 SQL 字符。下面的方法说明了如何确保在简单的 SQL 比较(等于、小于、大于)语句中使用的任何字符串都是安全的。此方法是通过确保在字符串中使用的任何撇号上再附加一个撇号进行转义来实现这一点。在 SQL 字符串中,两个连续的撇号被视为字符串内撇号字符的实例,而不是分隔符。 private string SafeSqlLiteral(string inputSQL)
{
return inputSQL.Replace("'", "''");
}
...
string safeSQL = SafeSqlLiteral(Login.Text);
SqlDataAdapter myCommand = new SqlDataAdapter(
"SELECT au_lname, au_fname FROM authors WHERE au_id = '" +
safeSQL + "'", myConnection);
|
其他最佳的做法
下面是用于减少安全漏洞以及用于将可能造成的破坏限制在一定范围内的一些其他措施:
| • |
通过限制输入的大小和类型,在入口(前端应用程序)防止无效输入。通过限制输入的大小和类型,可大大降低破坏的可能性。例如,如果数据库查找字段长度为 11 个字符并且全部由数字字符组成,则强制执行该规则。 |
| • |
用具有最少权限的帐户运行 SQL 代码。这样可以大大减轻可能造成的损害。 例如,如果用户要注入 SQL,以便从数据库中删除表,但是 SQL 连接所使用的帐户没有相应的权限,则 SQL 代码将失败。这是不应将 sa 帐户或 db_owner 的成员用于应用程序的 SQL 连接的又一原因。 |
| • |
在 SQL 代码中出现异常时,不要向最终用户暴露数据库引起的 SQL 错误。应记录错误信息,但只显示用户友好信息。这样可以避免泄露可能对攻击者有帮助的不必要的详细信息。 |