如何在没有 SQL 注入漏洞的情况下参数化 SQL 表

How can I parameterize an SQL table without vulnerability to SQL injection

提问人:Nigel 提问时间:11/18/2021 最后编辑:Nigel 更新时间:12/28/2021 访问量:885

问:

我正在编写一个 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 上快速搜索后,我发现了这个问题,它建议使用我的第一种方法(具有 sqli 漏洞的方法)。这根本无济于事,所以我继续搜索并找到了这个问题,它说唯一安全的解决方案是对可能的表进行硬编码。同样,这不适用于我的类库,它需要适用于任意表名。

我的问题是:如何在不对 SQL 注入造成漏洞的情况下参数化表名?

C# SQL 服务器 准备语句 SQL 注入

评论

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

答:

6赞 Aaron Bertrand 11/18/2021 #1

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

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

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

这要求您将表名作为适当的参数传入,而不是连接或标记替换。(请不要将 AddWithValue() 用于任何事情

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

因此,最终代码将是这样的:

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

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

评论

0赞 T.S. 11/18/2021
我还会添加条带字符。或者当这些被发现时踢出去。如果他们有,现在可以了; -- / *SELECT TOP 0 * FROM {tableName}SELECT TOP 0 * FROM sys.objects where 1=2; drop table Users
1赞 Aaron Bertrand 11/18/2021
@T.S. 处理这一点,即使有人设法首先创建了一个名为 的表,因为它首先必须通过对 的检查。如果您愿意,可以剥离字符,但这只是额外的工作,不会提供任何额外的保护,而不是 (a) 针对元数据进行验证和 (b) 使用 .QUOTENAME()[sys.objects where 1=2; drop table Users]sys.objectsQUOTENAME()
1赞 Charlieface 11/18/2021
@T.S.正如你所看到的,它在小提琴 dbfiddle.uk/ 中工作得很好......
3赞 sticky bit 11/18/2021 #2

可以将表名传递给 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()

0赞 Nigel 11/19/2021 #3

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;
}

评论

0赞 Aaron Bertrand 11/19/2021
它包含多个表,而不是“任何数据库”——我在应用程序代码中使用该逻辑的问题是,如果我作为 DBA 想要使我的检查更加严格,我不应该用我不懂的语言修改代码。
0赞 Nigel 11/19/2021
我的类库的目的是为任意数据库工作。它是一个独立的库。DBA 不仅不会有业务搞砸它,而且如果库是闭源的,他们甚至无法做到。我没有在问题中说明这一点,因为它与问题无关
1赞 Aaron Bertrand 11/19/2021
当然,您可以在没有存储过程的情况下以相同的方式解决问题。我的观点是,负责数据库的人可能与访问数据库的类库的作者有不同的目标(以及关于保护的不同想法)。:-)