提问人:Nigel 提问时间:11/18/2021 最后编辑:Nigel 更新时间:12/28/2021 访问量:885
如何在没有 SQL 注入漏洞的情况下参数化 SQL 表
How can I parameterize an SQL table without vulnerability to SQL injection
问:
我正在编写一个 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 注入造成漏洞的情况下参数化表名?
答:
任意表名仍然必须存在,因此您可以先检查它是否存在:
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
--
;
评论
; -- / *
SELECT TOP 0 * FROM {tableName}
SELECT TOP 0 * FROM sys.objects where 1=2; drop table Users
QUOTENAME()
[sys.objects where 1=2; drop table Users]
sys.objects
QUOTENAME()
可以将表名传递给 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()
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;
}
评论
sys.tables
tableName