执行动态SQL,可以使用EXEC(@sql),也可以存储过程sys.sp_executesql,由于参数是动态SQL暴露出来的,为了避免参数注入的风险,必须对参数做检查。相比EXEC(@sql),sys.sp_executesql是更安全的实现方式,因为后者会做参数的检查。

一,简单的SQL注入

简单举例,对于下面的动态SQL,由于没有对参数@tablename做检查,很容易导致SQL注入。



SET @sql = N'SELECT * FROM dbo.' + @tablename;
EXEC sys.sp_executesql @sql;


假如用户传递的参数值是:Users; DROP TABLE dbo.Users;

执行该动态SQL,会把删掉Users表。如何避免这种类型的SQL注入呢?解决方法有多种多样,比较简单的一种是使用QUOTENAME(),例如:



SET @sql = N'SELECT * FROM dbo.' + QUOTENAME(@tablename);


二,EXEC

把要执行的SQL语句转换成字符串,通过EXEC()函数来执行该语句,这是常规的方式,需要用户对参数进行检查:



EXEC(@sql)


根据我的经验,在编写动态SQL时,使用QUOTENAME()和REPLACE()更加安全和有效。当参数表示的表对象、视图对象或者字段时,建议使用QUOTENAME();当动态SQL的主题比较长时,可以设置占位符,比如##SQL_Block##,##Table_Name##,然后使用REPLACE()函数进行替换。

举个例子,先写好动态SQL的总体,把动态变化的参数用一个占位符来表示,最后用实际的参数来替换占位符:



set @sql = N'select * from ##table_name## ....';
set @sql=replace(@sql,N'##table_name##',N'[dbo].'+quotename(@tablename))


三,sys.sp_executesql

该存储过程实际上是对查询进行参数化处理,使得执行查询的主体不变,只有参数发生变化。该存储过程在执行动态SQL时会对参数和参数类型进行检查,可以避免通过参数传值引发的注入风险,该存储过程有三个参数,前两个参数是不变的,从第三个参数开始的参数,是动态变化的参数值:



sys.sp_executesql [ @stmt = ] statement  
[
{ , [ @params = ] N'@parameter_name data_type [ OUT | OUTPUT ][ ,...n ]' }
{ , [ @param1 = ] 'value1' [ ,...n ] }
]


参数注释:

  • @stmt:动态SQL主体,是NVARCHAR(max)类型
  • @params:对@stmt中使用的参数进行声明,是NVARCHAR(max)类型
  • @param1=value1, @param2=value2, ...:对参数传值

在编写动态SQL的主体时,查询中用到的变量可以不需要声明,在第二个参数中对@stmt中用到的变量进行声明,这种方式实际上是强制把参数抽取出来,把整个查询语句强制参数化处理,如下所示:



DECLARE @sqlCommand nvarchar(1000)
SET @sqlCommand = N'SELECT * FROM Person.Address WHERE City = @city'
EXECUTE sys.sp_executesql @sqlCommand, N'@city nvarchar(75)', @city = 'Shanghai'


使用抽取动态SQL主题,好处是提高查询性能,通过传递参数获得动态的结果,缺点是不够灵活,只能对变量进行参数化处理。