我继承了一个具有自定义对象关系映射(ORM)系统的代码库. sql无法在应用程序中编写,但ORM仍必须最终生成要发送到sql Server的sql.所有标识符都用双引号引用.
string QuoteName(string identifier) { return "\"" + identifier.Replace("\"","\"\"") + "\""; }
如果我在sql中构建这个动态sql,我会使用内置的sql Server QUOTENAME函数:
declare @identifier nvarchar(128); set @identifier = N'Client"; DROP TABLE [dbo].Client; --'; declare @delimitedIdentifier nvarchar(258); set @delimitedIdentifier = QUOTENAME(@identifier,'"'); print @delimitedIdentifier; -- "Client""; DROP TABLE [dbo].Client; --"
我没有找到任何关于如何在sql Server中转义带引号的标识符的权威文档.我找到了Delimited Identifiers (Database Engine),我也看到了this stackoverflow question关于消毒.
如果必须调用QUOTENAME函数只是为了引用那些不需要sql Server的大量流量的标识符.
关于sql注入,ORM似乎已经过深思熟虑了.它位于C#中,早于nHibernate端口和实体框架等.所有用户输入都是使用ADO.NET sqlParameter对象发送的,它只是我在这个问题中关注的标识符名称.这需要适用于sql Server 2005和2008.
2010-03-31更新
虽然应用程序不允许在查询中允许用户输入标识符名称,但ORM通过它对ORM样式读取和自定义查询具有的查询语法来执行.正是我试图最终阻止所有可能的sql注入攻击的ORM,因为它非常小并且易于验证而不是所有应用程序代码.
查询界面的一个简单示例:
session.Query(new TableReference("Client") .Restrict(new FieldReference("city") == "Springfield") .DropAllBut(new FieldReference("first_name"));
ADO.NET发送此查询:
exec sp_executesql N'SELECT "T1"."first_name" FROM "dbo"."Client" AS "T1" WHERE "T1"."city" = @p1;',N'@p1 nvarchar(30)',N'Springfield';
也许有必要考虑一下nHibernate查询语言(HQL)中的类似内容:
using (ISession session = NHibernateHelper.OpenSession()) { Client client = session .CreateCriteria(typeof(Client)) \\ <-- TableReference in example above .Add(Restrictions.Eq("city","Springfield")) \\ <-- FieldReference above .UniqueResult<Client>(); return client; }
也许我应该看看nHibernate如何保护输入.
解决方法
String.Format(@"declare @delimitedIdentifier nvarchar(258); set @delimitedIdentifier = {0};",QuoteName(identifier));
如果QuoteName(标识符)超过258个字符,则在分配给@delimitedIdentifier时将以静默方式截断它.当发生这种情况时,您可能会打开@delimitedIdentifier不正确转义的可能性.
微软的“安全软件开发人员”Bala Neerumalla有an MSDN article,它更深入地解释了这个主题.本文还包含了我发现的“关于如何在sql Server中转义引用标识符的权威文档”中最接近的内容:
The escaping mechanism is simply doubling up the occurrences of right square brackets. You don’t need to do anything with other characters,including left square brackets.
这是我目前使用的C#代码:
/// <summary> /// Returns a string with the delimiters added to make the input string /// a valid sql Server delimited identifier. Brackets are used as the /// delimiter. Unlike the T-sql version,an ArgumentException is thrown /// instead of returning a null for invalid arguments. /// </summary> /// <param name="name">sysname,limited to 128 characters.</param> /// <returns>An escaped identifier,no longer than 258 characters.</returns> public static string QuoteName(string name) { return QuoteName(name,'['); } /// <summary> /// Returns a string with the delimiters added to make the input string /// a valid sql Server delimited identifier. Unlike the T-sql version,/// an ArgumentException is thrown instead of returning a null for /// invalid arguments. /// </summary> /// <param name="name">sysname,limited to 128 characters.</param> /// <param name="quoteCharacter">Can be a single quotation mark ( ' ),a /// left or right bracket ( [] ),or a double quotation mark ( " ).</param> /// <returns>An escaped identifier,no longer than 258 characters.</returns> public static string QuoteName(string name,char quoteCharacter) { name = name ?? String.Empty; const int sysnameLength = 128; if (name.Length > sysnameLength) { throw new ArgumentException(String.Format( "name is longer than {0} characters",sysnameLength)); } switch (quoteCharacter) { case '\'': return String.Format("'{0}'",name.Replace("'","''")); case '"': return String.Format("\"{0}\"",name.Replace("\"","\"\"")); case '[': case ']': return String.Format("[{0}]",name.Replace("]","]]")); default: throw new ArgumentException( "quoteCharacter must be one of: ',\",[,or ]"); } }