【问题标题】:How can I use a temp table and parameters with sp_executesql?如何在 sp_executesql 中使用临时表和参数?
【发布时间】:2016-05-06 16:16:17
【问题描述】:

我使用受this SO question 启发的存储过程添加了快速而简单的整个数据库搜索。它工作正常,但我担心 SQL 注入。当然我用的是 SQL 参数:

 string query = GetUserInput();
 using (SqlConnection con = new SqlConnection(conString))
 {
        using (SqlCommand cmd = new SqlCommand("SearchAllTables", con))
        {
                cmd.CommandType = CommandType.StoredProcedure;

                cmd.Parameters.Add("@SearchStr", SqlDbType.VarChar).Value = query;

                con.Open();
                SqlDataReader reader = cmd.ExecuteReader();

但是,由于 SP 构建查询并执行它,SQL 注入仍然是可能的。相关部分:

CREATE PROC [dbo].[SearchAllTables] 
(
@SearchStr nvarchar(100)
)
AS
BEGIN

CREATE TABLE #Results (TableName nvarchar(370), ColumnName nvarchar(370), ColumnValue nvarchar(3630), TableId int)
--snip boring part

SET @SearchStr2 = QUOTENAME('%' + @SearchStr + '%','''')
INSERT INTO #Results
EXEC
(
    'SELECT ''' + @TableName +''', ''' + @TableName + '.' + @ColumnName + ''', LEFT(CONVERT(varchar(max), ' + @ColumnName + '), 3630), [id]
     FROM ' + @TableName + ' (NOLOCK) ' +
     ' WHERE CONVERT(varchar(max), ' + @ColumnName + ') LIKE ' + @SearchStr2 --This is vulnerable
)

--snip all the END

SELECT TableName, ColumnName, ColumnValue, TableId FROM #Results

我想将其更改为使用带参数的 sp_executesql 以防止 SQL 注入。我把它改成:

declare @query nvarchar(1000)
set @query= 'SELECT ''' + @TableName +''', ''' + @TableName + '.' + @ColumnName + ''', LEFT(CONVERT(varchar(max), ' 
            + @ColumnName + '), 3630), [id] FROM ' + @TableName + ' (NOLOCK) ' +
            ' WHERE CONVERT(varchar(max), ' + @ColumnName + ') LIKE @term'
INSERT INTO #Results            
EXEC sp_executesql @query, N'@term nvarchar(100)', @term=@SearchStr2;

但是我现在没有得到任何结果。我尝试在查询中包含“插入”并使用全局临时表但没有骰子。我可以做些什么来防止 SQL 注入并取回结果?还是我的方法有误?

【问题讨论】:

  • 您为什么允许您的用户搜索系统中的任何表?为什么用户甚至知道表名?您可以将表名包含在 QUOTENAME 中以帮助防止 sql 注入,但老实说,整个概念对我来说似乎有点偏离。如果您要使用 NOLOCK,则需要包含 WITH 关键字。不推荐使用它。当然,如果您想在搜索中获得准确的结果,您应该使用该提示,因为它有时会丢失数据。
  • 用户不提供表/列名,这些变量在程序的早期填充。用户的搜索词已被 QUOTENAME 包裹。
  • 您的查询构建方式在我看来好像行不通,因为您使用 QUOTENAME 包装了搜索值。这用于对象名称,而不是值。你在底部编码的方式看起来是正确的,只需去掉你在提供的搜索词周围使用引号的部分。调试这种类型的动态 sql 的唯一方法是使用打印/选择语句。如果您查看 @query,您会看到您使用 quotename 引入的错误。
  • 删除 QUOTENAME 有效,但我想了解错误。当我保留 QUOTENAME 并打印查询时,如果我搜索“test”,它会显示为 LIKE '%test%' ,这对我来说很好。
  • 不仔细看。它现在在您的值周围有单引号,因为您使用了引号名称。而且您在动态 sql 中使用了一个参数,因此它正在寻找像 '%test%' 这样的值并且它不会找到任何东西,因为该单引号现在是搜索条件的一部分。

标签: c# sql-server


【解决方案1】:

table names 和列名等某些内容不能作为动态查询中的参数发送,因此必须附加它们。当然,这很混乱(而且容易出错)。

由于您使用的是 C#,我建议您查看 Dynamic Linq library。它提供了一些扩展,允许在 LINQ 查询中使用字符串查询。其他生成动态查询的方法见here

好的,回到你最初的问题。

1) Dynamic Linq 应该允许您轻松编写查询,例如:

// this requires C# 6.0 to use interpolated strings. String.Format can be used instead
someRepository.GetAll.Where($"{col1} = {value1} And {col2} = {value2}");

因此,您有动态列和值,但您需要动态表。一种方法是根据提供的类型动态获取存储库:

// this contains repositories for all types mapped to used tables
public class UnitOfWork : IUnitOfWork
{
    public IRepository<Table1> Table1Repository { get; private set; }
    public IRepository<Table2> Table2Repository { get; private set; }
    // other come here

    // all these are injected
    public UnitOfWork(IDbContext context, IRepository<Table1> table1Repository, IRepository<Table2> table2Repository
    {
        Table1Repository = table1Repository;
        Table2Repository = table2Repository;
        // other initializations
    }

    // typed version
    public IRepository<T> GetRepository<T>()
        where T: class
    {
        Type thisType = this.GetType();
        foreach (var prop in thisType.GetProperties())
        {
            var propType = prop.PropertyType;

            if (!typeof(IRepository).IsAssignableFrom(propType))
                continue;

            var repoType = propType.GenericTypeArguments[0];
            if (repoType == typeof(T))
                return (IRepository<T>) prop.GetValue(this);
        }

        throw new ArgumentException(String.Format("No repository of type {0} found", typeof(T).FullName));
    }

    // dynamic type version (not tested, just written here)
    public IRepository GetRepository(Type type)
        where T: class
    {
        Type thisType = this.GetType();
        foreach (var prop in thisType.GetProperties())
        {
            var propType = prop.PropertyType;

            if (!typeof(IRepository).IsAssignableFrom(propType))
                continue;

            var repoType = propType.GenericTypeArguments[0];
            if (repoType == type)
                return (IRepository) prop.GetValue(this);
        }

        throw new ArgumentException(String.Format("No repository of type {0} found", typeof(T).FullName));
    }
}

要动态获取存储库,您需要在表名(或过滤器值中的表标识符与该类型)之间建立映射。比如:

var filterTableMap = new Dictionary<String, Type>()
    {
        { "Table1", typeof(Table1Repository) },
        { "Table2", typeof(Table2Repository) },
        // and so on
    };

您的情况如下所示:

var query = filterTableMap["Table1"].GetAll.Where($"{col1} = {value1}");

但是,如果您想一次应用多个条件,这将非常棘手。

2) 一个有趣的方法是使用reflection:

// this is a slightly changed and not tested version from the source
public static IEnumerable<T> WhereQuery(this IEnumerable<T> source, string columnName, string propertyValue)
{
    return source.Where(m => { 
        return m.GetType().GetProperty(columnName).GetValue(m, null).ToString().Contains(propertyValue); 
    });
}

这应该允许像这样链接 where 条件:

var query = filterTableMap["Table1"].GetAll.WhereQuery("col1", value1);
if (value2 != null)
    query = query.WhereQuery("col2", value2);

但是,我不认为 LINQ2SQL 可以为那个 Where 生成 SQL,所以源必须是对象列表。如果没有事先过滤数据以减少长度,这是一个严重的问题。

3) 正如here 所指出的,表达式树似乎是最佳选择。比如:

var param = Expression.Parameter(typeof(String));
var condition =
    Expression.Lambda<Func<String, bool>>(
        Expression.Equal(
            Expression.Property(param, "Col1"),
            Expression.Constant("Value1", typeof(String))
        ),
        param
    ).Compile(); 
// for LINQ to SQL/Entities skip Compile() call

var query = source.Where(condition);

对于包含的解决方案更加复杂,如图here

我看到在 C# 中对过滤进行建模的优点是:

  1. 没有乱码
  2. 没有SQL注入(但可能导致LINQ injection
  3. 如果使用存储库和依赖注入,则有助于进行单元测试
  4. 更容易维护

缺点:

  1. 更复杂
  2. 可能的性能问题

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-04-14
    相关资源
    最近更新 更多