我如何参数化 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()
,正如我在这些帖子中解释的那样:
- 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 除外从另一个 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;
}
我正在编写一个 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()
,正如我在这些帖子中解释的那样:
- 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 除外从另一个 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;
}