在.NET中的动态SQL中如何对表/列名进行清理?(防止SQL注入攻击)

12
我是一个动态生成SQL语句的程序员,希望确保我的代码免受SQL注入攻击。
为了举例说明,这里有一个最简单的生成方式:
var sql = string.Format("INSERT INTO {0} ({1}) VALUES (@value)",
    tableName, columnName);

在上述代码中,tableNamecolumnName和绑定到@value的任何内容都来自不受信任的来源。由于使用了占位符,@value免受SQL注入攻击的影响,可以忽略它。(命令通过SqlCommand执行。)
然而,tableNamecolumnName不能作为占位符绑定,因此容易受到注入攻击。由于这是一个“真正动态”的场景,没有可用的tableNamecolumnName白名单。
因此,问题是:
是否有一种标准的内置方法来检查和/或净化tableNamecolumnName?(SqlConnection、助手类等)如果没有,有什么好的方法可以在不使用第三方库的情况下执行此任务?
注意:
  • 所有的SQL标识符,包括模式,都应该被接受:例如[模式]。[我的表]。列table1一样“安全”。
  • 可以对标识符进行清理检测无效标识符。(不需要确保表/列在上下文中实际上是有效的;生成的SQL可能无效,但必须是“安全”的。)

更新:

刚刚发现这个,觉得有些有趣:.NET4(EF4?)中有一个SqlFunctions.QuoteName函数。好吧,它并没有真正帮助我在这里……

3个回答

27

我不确定你是否仍在研究这个问题,但是 DbCommandBuilder 类提供了一个名为 QuoteIdentifier 的方法来实现此目的。其主要优点是它与数据库无关且不涉及任何正则表达式混乱。

从 .NET 4.5 开始,您只需要使用 DbConnection 对象就可以对表和列名进行净化处理。

DbConnection connection = GetMyConnection(); // Could be SqlConnection
DbProviderFactory factory = DbProviderFactories.GetFactory(connection);

// Sanitize the table name
DbCommandBuilder commandBuilder = factory.CreateCommandBuilder();

string tableName = "This Table Name Is Long And Bad";
string sanitizedTableName = commandBuilder.QuoteIdentifier(tableName);

IDbCommand command = connection.CreateCommand();
command.CommandText = "SELECT * FROM " + sanitizedTableName;

// Becomes 'SELECT * FROM [This Table Name Is Long And Bad]' in MS-SQL,
// 'SELECT * FROM "This Table Name Is Long And Bad"' in Oracle, etc.

(在4.5版本之前,您需要其他方法来获取DbProviderFactory -- 可能是从应用程序配置中的数据提供程序名称或硬编码的位置。)


啊,是的。那很有道理——我正在使用LINQ2SQL,所以..是的,总是SQL Server :) - user166390
有没有办法在不需要数据库连接对象的情况下完成这个操作? - Jay Sullivan
1
@notfed 是的,如果你知道要连接哪种类型的数据库。你可以直接实例化正确类型的DbProviderFactory(例如,如果你正在使用SQL Server,你可以使用var factory = new SqlClient.SqlClientFactory()),而不是第2行。 - Jeremy Todd
根据此链接:https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlcommandbuilder.quoteidentifier(v=vs.110).aspx,SqlCommandBuilder.QuoteIdentifier自.NET 2.0以来就已经可用。这是我用来清理表/列名称输入的方法。只需实例化一个SqlCommandBuilder实例并使用QuoteIdentifier方法来清理输入即可。 - Ian
DbCommandBuilder.QuoteIdentifier 能保护你免受嵌入分号的影响吗? - Mike
显示剩余3条评论

4

因为您正在使用SqlConnection,所以假设这是一个SQL Server数据库。

基于这个假设,您可以使用一个正则表达式来验证表和字段名称,该正则表达式遵循在MSDN中定义的 SQL Server标识符规则。虽然我对正则表达式非常生疏,但我找到了这一个应该足够接近:

[\p{L}{\p{Nd}}$#_][\p{L}{\p{Nd}}@$#_]*

然而,正则表达式不能解决SQL Server关键字问题,并且它不能保证表格和/或列实际存在(尽管你表示这并不是一个大问题)。
如果这是我的应用程序,我会首先确保最终用户没有尝试通过拒绝任何包含分号(;)的请求来执行注入。
接下来,我会通过删除有效名称分隔符(",',[,]),按点拆分表名以查看是否指定了模式,并对INFORMATION_SCHEMA.TABLES执行查询以确定表格是否存在。
例如:
SELECT 1 
FROM   INFORMATION_SCHEMA.TABLES 
WHERE  TABLE_NAME = 'tablename' 
AND    TABLE_SCHEMA = 'tableschema'

如果您使用参数创建此查询,则应进一步保护自己免受注入攻击。
最后,我将验证每个列名是否存在,通过类似的步骤进行验证,只是在确定表格有效后使用INFORMATION_SCHEMA.COLUMNS来确定列(s)的有效性。
我可能会从SQL Server获取此表的有效列列表,然后在代码中验证每个请求列是否在列表中。这样,您可以准确地了解哪些列存在错误,并向用户提供该反馈信息。

你说得对,确实是SQL Server。我倾向于采用最小化的正则表达式路线,但我希望存在一些预制的(并经过测试的;-))东西。我真的很喜欢检查它与模式元数据相匹配的额外想法。 - user166390
如果您使用参数化查询来测试表/模式,则在代码中检查每个列名是否存在于表的完整列名列表中,那么您实际上不需要对传入值执行任何有效性检查,这将是最小化的终极方案 :)。 - competent_tech
嗯,这是一个稍微庞大一些的实现 :) - user166390
感谢您的输入。我们现在使用的方法(不够智能,无法查看模式)是将其拆分为组件,删除除字母数字和正常空格以外的所有内容,然后重新以“[]”形式连接起来。当然,它会对像[a . b]这样的东西表现出“意外”的行为(将其转换为[a ].[ b]),但我不确定前者甚至后者是否有效... - user166390
1
查看 https://msdn.microsoft.com/en-us/library/ms175874.aspx,标识符不能以 $ 开头,但可以以 @ 开头。我现在正在使用 ^[\p{L}@#][\p{L}\p{Nd}$@#]*$。 - Serge van den Oever

4
对于 SQL Server 来说,清理标识符相当简单:
// To make a string safe to use as an SQL identifier :
// 1. Escape single closing bracket with double closing bracket
// 2. Wrap in square brackets
string.Format("[{0}]", identifier.Replace("]", "]]"));

一旦用括号括起来并转义,唯一不能作为标识符的是空字符串/ null。


我不知道为什么你被点了反对,因为这正是 DbCommandBuilder.QuoteIdentifier 做的事情。 - user247702

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接