如何将表名传递给存储过程?

21

我刚遇到了一个奇怪的事情......我们网站上有一些代码,它将一个巨大的 SQL语句传递给 SQL Server 作为查询,并通过一些搜索和替换来根据一些用户值在代码中进行修改,然后将其传递给 SQL Server。

我本来认为将其作为存储过程的参数化查询会更加清晰,以用户值作为参数,但当我仔细看时,我明白了他们这么做的原因......他们所选择的表是可变的,取决于这些用户值。

例如,在某种情况下,如果值为(“FOO”,“BAR”),则查询最终可能类似于“SELECT * FROM FOO_BAR”

有没有一种简单明了的方法来解决这个问题?我试过的所有方法都不太优雅。

编辑:当然,我可以在存储过程中动态生成sql语句并执行它(呕吐),但那时我想知道我是否获得了任何好处。

编辑2:以某种智能的方式重构表名,比如将它们全部放在一个表格中,并使用新列来表示不同的名称,将是解决所有这些问题的好方法,正如一些人直接指出或暗示的那样。可惜在这种情况下不是一个选项。


SQL-Server 2008 允许使用 TABLE 变量。 - Lance Roberts
2
@Lance:没错,确实是这样,但我认为(可能我错了),它们的工作方式与此不同。表变量存储表数据,而不是表名。这类似于临时表的概念。 - Beska
您不必“重构表名”,因为它们已经在系统提供的视图INFORMATION_SCHEMA.TABLES中了。 - RBarryYoung
12个回答

53

首先,你绝对不应该像这样在客户端应用程序上进行 SQL 命令组合,因为这就是 SQL 注入攻击的来源。(如果是一个没有自己权限的管理员工具,则无妨,但不能用于共享使用的应用程序)。

其次,是的,调用存储过程的参数化方式既更清晰又更安全。

然而,由于你需要使用动态 SQL 来实现此操作,你仍然不希望将传递的字符串包含在执行查询的文本中。相反,你想要使用传递的字符串来查找用户应该被允许以何种方式查询的实际表格名称

这里提供一个简单的幼稚例子:

CREATE PROC spCountAnyTableRows( @PassedTableName as NVarchar(255) ) AS
-- Counts the number of rows from any non-system Table, *SAFELY*
BEGIN
    DECLARE @ActualTableName AS NVarchar(255)

    SELECT @ActualTableName = QUOTENAME( TABLE_NAME )
    FROM INFORMATION_SCHEMA.TABLES
    WHERE TABLE_NAME = @PassedTableName

    DECLARE @sql AS NVARCHAR(MAX)
    SELECT @sql = 'SELECT COUNT(*) FROM ' + @ActualTableName + ';'

    EXEC(@SQL)
END

有些人问为什么这样更安全。希望通过小博比表格能更清楚地解释:

0 alt text

更多问题的答案:

  1. 单独使用 QUOTENAME 不能保证安全。微软鼓励我们使用它,但他们没有保证黑客无法突破。FYI,真正的安全性在于保证。使用 QUOTENAME 的表查找是另一回事,它是不可打破的。

  2. 对于此示例来说,严格来讲 QUOTENAME 不是必需的,仅 INFORMATION_SCHEMA 中的 Lookup 翻译通常就足够了。在这里包括完整和正确的解决方案是安全性上的良好习惯,实际上,QUOTENAME 在这里是保护免受另一个不同但类似的潜在问题,称为 latent injection


我应该指出,您也可以使用动态列名和 INFORMATION_SCHEMA.COLUMNS 表来做同样的事情。

您还可以通过使用参数化的 SQL 查询而不是存储过程来避免需要,(请参阅此处:https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlcommand.parameters?view=netframework-4.8)。但我认为对于这种情况,存储过程提供了更可管理且易于出错的安全设施。


1
请看我的评论,针对AlexS的回答。 - Lance Roberts
3
哦,是的......我喜欢小鲍比·泰布尔斯。我在我的办公隔间里贴了那个相当长一段时间。 - Beska
Beska:我认为你把漏洞和攻击混淆了。注入是指未经验证的文本被提升到命令流中,这是开发人员(也就是我们)无意中创建的漏洞。而攻击则是指有人故意尝试利用它。 - RBarryYoung
1
@RBarry。我删除了之前的评论,因为我意识到第二部分是错误的,但在这种情况下,您不需要手动添加方括号,这让我感到困惑。QUOTENAME('foo') = [foo] - Martin Smith
1
@MichaelFreidgeim 是的。 - RBarryYoung
显示剩余19条评论

5

(不)幸运的是,没有办法做到这一点 - 除了用于动态SQL生成之外,您不能将表名作为参数传递给存储代码。在决定在哪里生成SQL代码时,我更喜欢应用程序代码而不是存储代码。应用程序代码通常更快且更易于维护。

如果您不喜欢正在使用的解决方案,我建议进行更深入的重新设计(即更改模式/应用程序逻辑,以便您不再需要在任何地方传递表名作为参数)。


嗯,不是我不喜欢它,而是我希望有更好的方法来解决它。如果当前的解决方案(在代码中修改SQL字符串)是最好的方法,那么如果我想成为一名优秀的程序员,我最好喜欢它,对吧? - Beska
@RBarryYoung:我建议进行“更深层次的重新设计……这样你就不必再将表名作为参数传递”。我猜这不是“SQL注入”;-) - AlexS
3
使用动态SQL并不等同于“SQL注入”,它只是一种容易受到“SQL注入”攻击的方法,需要小心谨慎地实现。 - Lance Roberts
AlexS说:“在决定生成SQL代码的位置时,我更喜欢应用程序代码而不是存储代码。”这是我的关注点。“更深层次的重新设计”当然是一个很好的建议。 - RBarryYoung
@AlexS 和 @RBarryYoung... 从概念上来说,深层次的重新设计是一个好主意,但遗憾的是,在这里不可行。 - Beska
显示剩余2条评论

2
我认为不应在存储过程中动态生成SQL; 这会导致问题并可能引起注入漏洞。
相反,我会分析所有可能受查询影响的表,并创建某种枚举来确定用于查询的表。

我考虑过这个问题,但是问题在于可能会有许多(大约50个)表格需要进行此操作,而更为关键的是,这些表格还会不断地被添加。 - Beska
我对SQL注入并不过分担心,因为我们从未向用户查询任何将进入参数的内容...它们都是用户无法访问的内部值。但是,比我更好的人可能认为它们是安全的,而实际上并非如此,所以这个警告很有必要。 - Beska
他已经在客户端代码中遭受了SQL注入攻击。在那里并不比在SQL Server中更安全。而且,完全可以使用动态SQL来解决这个问题,而不会有任何注入的风险。 - RBarryYoung
看起来@RBarry Young的答案可能是最好的选择。它是动态SQL,但足够“安全”,让您确定表名。 - Randolpho

2

听起来你最好使用ORM解决方案。

当我看到存储过程中的动态SQL时,我感到不安。


有时候是不可避免的,比如使用PIVOT命令。 - BoltBait
没错,但通常你发现自己没有做对的方法就是被黑客攻击了。 - ScottE
1
@RBarryYoung:没错,只要安全地实现了,这样做完全没问题。但是当你看到这样的代码时,你仍然应该感到不舒服。如果你正在这样做,很可能你本可以更好地设计系统架构。 - Randolpho

1

你可以考虑使用一个包含相同 SQL 命令的 case 语句,每个有效表格都有一个,然后将表格名称作为字符串传递到此过程中,并让 case 选择要运行的命令。

顺便说一下,作为安全人员,上面的建议告诉你从系统表中选择以确保您拥有有效的表格,对我来说似乎是一种浪费的操作。如果有人可以通过 QUOTENAME() 注入,则注入也可以在系统表上正常工作,就像在底层表上一样。这唯一有用的事情就是确保它是一个有效的表格名称,我认为上面的建议是更好的方法,因为根本不使用 QUOTENAME()。


这个最后的观察是错误的。RBarryYoung编写的查询不是动态的,也不会进行任何更新,也不会使用不可信数据填充任何变量。因此,不存在任何直接、潜在或持久的SQL注入风险。这与执行EXEC('UPDATE ' + @tablename + ' SET...')非常不同。不仅没有风险,而且这是使整个过程安全的关键要素。 - EGP

0

实际上,我想知道如何将表名传递给存储过程以创建表。通过阅读一些答案并尝试在我的端口进行一些修改,我最终能够创建一个以参数形式传递名称的表。以下是存储过程,供其他人检查其中的任何错误。

USE [数据库名称] GO /****** 对象:StoredProcedure [dbo].[sp_CreateDynamicTable] 脚本日期:06/20/2015 16:56:25 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE PROCEDURE [dbo].[sp_CreateDynamicTable] @tName varchar(255) AS BEGIN SET NOCOUNT ON; DECLARE @SQL nvarchar(max)

SET @SQL = N'CREATE TABLE [DBO].['+ @tName + '] (DocID nvarchar(10) null);'

    EXECUTE sp_executesql @SQL

结束


0

@RBarry Young 您不需要在查询字符串中添加括号到@ActualTableName,因为它已经包含在INFORMATION_SCHEMA.TABLES中的查询结果中。否则,在执行过程中会出现错误。

CREATE PROC spCountAnyTableRows( @PassedTableName as NVarchar(255) ) AS -- 安全地计算任何非系统表的行数 BEGIN DECLARE @ActualTableName AS NVarchar(255)

SELECT @ActualTableName = QUOTENAME( TABLE_NAME )
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = @PassedTableName

DECLARE @sql AS NVARCHAR(MAX)
--SELECT @sql = 'SELECT COUNT(*) FROM [' + @ActualTableName + '];'

-- changed to this
SELECT @sql = 'SELECT COUNT(*) FROM ' + @ActualTableName + ';'

EXEC(@SQL)

结束


0

我会尽量避免使用动态SQL。

虽然不是最优雅的解决方案,但完美地完成了工作。

PROCEDURE TABLE_AS_PARAMTER (
        p_table_name IN VARCHAR2
    ) AS
    BEGIN
        CASE p_table_name
            WHEN 'TABLE1' THEN
                UPDATE TABLE1
                SET
                    COLUMN1 =1
                WHERE
                    ID =1;
            WHEN 'TABLE2' THEN
                UPDATE TABLE1
                SET
                    COLUMN1 =1
                WHERE
                    ID =2;
        END CASE;

        COMMIT;
    EXCEPTION
        WHEN OTHERS THEN
            ROLLBACK
END TABLE_AS_PARAMTER;

0

根据表格中列的集合是相同还是不同,我会从长远来看采用两种方法:

1)如果它们相同,为什么不创建一个新列,将其用作选择器,并从用户提供的参数中派生其值?(这是否是性能优化?)

2)如果它们不同,处理它们的机会也不同。因此,将选择/处理代码拆分为单独的块,然后分别调用它们似乎对我来说是最模块化的方法。您将重复“选择*from”部分,但在这种情况下,表的集合有限。

允许调用代码提供表名的两个任意部分进行选择的操作非常危险。


遗憾的是,虽然我可以使用枚举来决定要从哪个表中进行选择,但这样会非常庞大(现在大约有50个表),更糟糕的是,还会不断添加更多的表,这将导致维护问题。 - Beska
你更多的是属于(1)的情况,而不是(2)的情况吗?例如,像一个包含“名字,姓氏”的表列表,表的名称为"程序员","经理","主管","用户"等等。 - Andrew Y

0

我不知道你为什么要将数据分散到几个表中,但这听起来像是你违反了基本原则之一。数据应该在表中,而不是作为表名。

如果这些表的布局大致相同,可以考虑把数据放在一个单独的表中。这将解决动态查询的问题,并使数据库布局更加灵活。


我理论上同意,但现在做不到。这是一个商务服务器问题。我不完全理解其中的所有细节,但这是一个庞大的网站,其中各种目录表是其核心部分。短期内我们无法改变它。 - Beska

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接