如何参数化 SQL 表而不会受到 SQL 注入的影响

Posted

技术标签:

【中文标题】如何参数化 SQL 表而不会受到 SQL 注入的影响【英文标题】:How can I parameterize an SQL table without vulnerability to SQL injection 【发布时间】:2021-12-28 23:11:09 【问题描述】:

我正在编写一个 C# 类库,其中一个特性是能够创建一个与任何现有表的架构匹配的空数据表。

例如,这个:

private DataTable RetrieveEmptyDataTable(string tableName)

    var table = new DataTable()  TableName = tableName ;

    using var command = new SqlCommand($"SELECT TOP 0 * FROM tableName", _connection);
    using SqlDataAdapter dataAdapter = new SqlDataAdapter(command);
    dataAdapter.Fill(table);

    return table;

上面的代码可以工作,但它有一个明显的安全漏洞:SQL 注入。

我的第一直觉是像这样参数化查询:

    using var command = new SqlCommand("SELECT TOP 0 * FROM @tableName", _connection);
    command.Parameters.AddWithValue("@tableName", tableName);

但这会导致以下异常:

必须声明表变量“@tableName”

在 Stack Overflow 上快速搜索后,我找到了this question,它建议使用我的第一种方法(带有 sqli 漏洞的方法)。这根本没有帮助,所以我一直在搜索并找到this question,它说唯一安全的解决方案是对可能的表进行硬编码。同样,这不适用于我的类库,它需要适用于任意表名。

我的问题是:如何参数化表名而不会受到 SQL 注入的影响?

【问题讨论】:

SQL Server 不允许参数化的表名——你必须根据你的显示方式来做。在我上次的演出中,我们提出了许多可能的解决方案(因为我们所做的第一个查询是“哪个数据库和哪个表包含该客户的主数据”)。我们提出的一种解决方案是进行特定的表替换调用,检查表以查看它是否在数据库中的表列表中(来自sys.tables,已缓存)。最后,我们决定接受风险并保持简单 @Flydog57 这也是我现在的方向。在连接到查询之前可能会进行一些清理尝试 如果您和我们一样,tableName 是我们从存储客户元数据的表中获取的字符串。因为它完全在我们的控制之下,并且不受用户输入或任何其他受污染的数据的影响,所以感觉没有漏洞并且不值得添加复杂性。我仍然希望在我们的数据层中使用单独的令牌格式和单独的方法来进行表名替换(它只是让我感到困扰)。我输了 【参考方案1】:

Aaron Bertrand 的回答解决了这个问题,但是存储过程对于可能与任何数据库交互的类库没有用处。这是使用他的写RetrieveEmptyDataTable(我的问题的方法)的方法 答案:

private DataTable RetrieveEmptyDataTable(string tableName)

    const string tableNameParameter = "@TableName";
    var query =
        "  IF EXISTS (SELECT 1 FROM sys.objects\n" +
        $"      WHERE name = tableNameParameter)\n" +
        "  BEGIN\n" +
        "    DECLARE @sql nvarchar(max) = N'SELECT TOP 0 * \n" +
        $"      FROM ' + QUOTENAME(tableNameParameter) + N';';\n" +
        "    EXEC sys.sp_executesql @sql;\n" +
        "END";


    using var command = new SqlCommand(query, _connection);
    command.Parameters.Add(tableNameParameter, SqlDbType.NVarChar).Value = tableName;
    using SqlDataAdapter dataAdapter = new SqlDataAdapter(command);
    var table = new DataTable()  TableName = tableName ;
    Connect();
    dataAdapter.Fill(table);
    Disconnect();
    return table;

【讨论】:

它有几个表,而不是“任何数据库” - 我在应用程序代码中使用该逻辑的问题是,如果我作为 DBA 想让我的检查更加严格,我不应该必须用我不懂的语言修改代码。 我的类库的目的是为任意数据库工作。这是一个独立的图书馆。不仅 DBA 不会有任何麻烦,而且如果图书馆是闭源的,他们甚至都无法做到。我没有在问题中说明这一点,因为它与问题无关 你当然可以不用存储过程用同样的方法解决问题。我的观点是,负责数据库的人可能与访问数据库的类库的作者有不同的目标(以及关于保护的不同想法)。 :-)【参考方案2】:

任意表名仍然必须存在,因此您可以先检查它是否存在:

IF EXISTS (SELECT 1 FROM sys.objects WHERE name = @TableName)
BEGIN
  ... do your thing ...
END

此外,如果您希望允许用户从中选择的表列表是已知且有限的,或者匹配特定命名约定(如dbo.Sales%),或属于特定模式(如Reporting) ,您可以添加其他谓词来检查这些谓词。

这要求您将表名作为适当的参数传递,而不是连接或令牌替换。 (还有please don't use AddWithValue() for anything,永远。)

一旦您检查了对象的真实性和有效性,您仍然需要动态构建 SQL 查询,因为您仍然无法参数化表名。不过,正如我在这些帖子中解释的那样,您仍然应该申请 QUOTENAME()

Protecting Yourself from SQL Injection in SQL Server - Part 1 Protecting Yourself from SQL Injection in SQL Server - Part 2

所以最终的代码应该是这样的:

CREATE PROCEDURE dbo.SelectFromAnywhere
  @TableName sysname 
AS
BEGIN
  IF EXISTS (SELECT 1 FROM sys.objects
      WHERE name = @TableName)
  BEGIN
    DECLARE @sql nvarchar(max) = N'SELECT * 
      FROM ' + QUOTENAME(@TableName) + N';';
    EXEC sys.sp_executesql @sql;
  END
  ELSE
  BEGIN
    PRINT 'Nice try, robot.';
  END
END
GO

如果您还希望它出现在某个已定义的列表中,您可以添加

AND @TableName IN (N't1', N't2', …)

或者LIKE <some pattern> 或者加入sys.schemas 或者你有什么。

如果没有人有权修改程序以更改检查,则没有任何值可以传递给@TableName,这将允许您做任何恶意的事情,除了可能从您没想到的另一个表中进行选择因为有太多访问权限的人能够在调用代码之前创建。替换 --; 之类的字符不会让这更安全。

【讨论】:

我还会添加删除 ; -- / * 字符。或者当这些发现时踢出去。如果他们有SELECT TOP 0 * FROM tableName,现在可以是SELECT TOP 0 * FROM sys.objects where 1=2; drop table Users @T.S. QUOTENAME() 负责这一点,即使有人设法首先创建了一个名为 [sys.objects where 1=2; drop table Users] 的表,因为它首先必须通过对 sys.objects 的检查。您可以根据需要去除字符,但这只是额外的工作,不会提供任何额外的保护(a)验证元数据和(b)使用QUOTENAME() @T.S.如您所见,它在小提琴dbfiddle.uk/… 中运行良好【参考方案3】:

您可以将表名传递给 SQL Server 以在其上应用 quotename() 以正确引用它,然后仅使用引用的名称。

类似的东西:

...

string quotedTableName = null;

using (SqlCommand command = new SqlCommand("SELECT quotename(@tablename);", connection))

    SqlParameter parameter = command.Parameters.Add("@tablename", System.Data.SqlDbType.NVarChar, 128 /* nvarchar(128) is (currently) equivalent to sysname which doesn't seem to exist in SqlDbType */);
    parameter.Value = tableName;
    object buff = command.ExecuteScalar();
    if (buff != DBNull.Value
        && buff != null /* theoretically not possible since a FROM-less SELECT always returns a row */)
    
        quotedTableName = buff.ToString();
    


if (quotedTableName != null)

    using (SqlCommand command = new SqlCommand($"SELECT TOP 0 FROM  quotedTableName ;", connection))
    
        ...
    

...

(或者直接在 SQL Server 上做动态部分,也使用quotename()。但这似乎过于繁琐且不必要,尤其是如果您要在不同位置对表进行多个操作。)

【讨论】:

以上是关于如何参数化 SQL 表而不会受到 SQL 注入的影响的主要内容,如果未能解决你的问题,请参考以下文章

这个参数化查询是不是对 SQL 注入开放?

实体框架+sql注入

这个参数化查询如何防止 SQL 注入?

如何绕过Javascript中的sql注入错误

参数化查询如何帮助防止 SQL 注入?

参数化查询如何帮助防止 SQL 注入?