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

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

我正在编写一个 C# class 库,其中一个功能是能够创建与任何现有 table 的模式匹配的空数据 table。

例如,这个:

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

但这会导致以下异常:

Must declare the table variable "@tableName"

在 Stack Overflow 上快速搜索后,我发现 this question, which recommends using my first approach (the one with sqli vulnerability). That doesn't help at all, so I kept searching and found this question,它表示唯一安全的解决方案是对可能的 table 进行硬编码。同样,这对我的 class 库不起作用,它需要为任意 table 名称工作。

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

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

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

此外,如果您希望允许用户 select 的 table 列表已知且有限,或者符合特定的命名约定(如 dbo.Sales%) ,或属于特定模式(如 Reporting),您可以添加额外的谓词来检查这些。

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

一旦您检查对象是否真实有效,您仍然需要动态构建 SQL 查询,因为您仍然无法参数化 table姓名。不过,您仍然应该申请 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或你有什么。

如果没有人有权修改程序以更改支票,则您无法将任何值传递给 @TableName,这将使您可以做任何恶意的事情,也许 select 除外从另一个 table 你没想到,因为拥有太多访问权限的人能够在调用代码之前创建。替换像 --; 这样的字符并不能使它更安全。

您可以将 table 名称传递给 SQL 服务器以在其上应用 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 服务器上执行动态部分,也使用 quotename()。但这似乎过于繁琐且不必要,特别是如果您要在 table在不同的地方。)

Aaron Bertrand 的回答解决了这个问题,但是存储过程对于可能与任何数据库交互的 class 库没有用处。这是使用他的 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;
}