防止动态 SQL 中的 SQL 注入

iri*_*ias 9 security sql-server

让我们想象一个存储过程,它检索数据并进行某种分页。这个过程有一些输入,描述了我们想要的数据集以及我们如何对其进行排序。

这是一个非常简单的查询,但让我们以它为例。

create table Persons(id int, firstName varchar(50), lastName varchar(50))
go
create procedure GetPersons @pageNumber int = 1, @pageSize int = 20, @orderBy varchar(50) = 'id', @orderDir varchar(4) = 'desc'
as

declare @sql varchar(max)
set @sql = 'select id, firstName, lastName
from (
    select id, firstName, LastName, row_number() over(order by '+@orderBy+' '+@orderDir+') as rn
    from Persons
    ) t
where rn > ('+cast(@pageNumber as varchar)+'-1) * '+cast(@pageSize as varchar)+'
        and rn <= '+cast(@pageNumber as varchar)+' * '+cast(@pageSize as varchar)+' 
order by '+@orderBy+' '+@orderDir

exec(@sql)
Run Code Online (Sandbox Code Playgroud)

它应该像这样使用:

exec GetPersons @pageNumber = 1, @pageSize = 20, @orderBy = 'id', @orderDir = 'desc'
Run Code Online (Sandbox Code Playgroud)

但是一个聪明的人可以启动:

exec GetPersons @pageNumber = 1, @pageSize = 20, @orderBy = 'id)a from Persons)t;delete from Persons;print''', @orderDir = ''
Run Code Online (Sandbox Code Playgroud)

...并删除数据

这显然不是一个安全的情况。我们怎样才能防止它?

注意:这个问题不是关于“它是一种分页的好方法吗?” 也不是“做动态 sql 是件好事吗?”。问题是在动态构建 sql 查询时防止代码注入,以便在我们将来必须再次执行类似的存储过程时,有一些指导方针使代码更清晰。

一些基本思想:

验证输入

create procedure GetPersons @pageNumber int = 1, @pageSize int = 20, @orderBy varchar(50) = 'id', @orderDir varchar(4) = 'desc'
as

if @orderDir not in ('asc', 'desc') or @orderBy not in ('id', 'firstName', 'lastName')
begin
    raiserror('Cheater!', 16,1)
    return
end

declare @sql varchar(max)
set @sql = 'select id, firstName, lastName
from (
    select id, firstName, LastName, row_number() over(order by '+@orderBy+' '+@orderDir+') as rn
    from Persons
    ) t
where rn > ('+cast(@pageNumber as varchar)+'-1) * '+cast(@pageSize as varchar)+'
        and rn <= '+cast(@pageNumber as varchar)+' * '+cast(@pageSize as varchar)+' 
order by '+@orderBy+' '+@orderDir

exec(@sql)
Run Code Online (Sandbox Code Playgroud)

传递 ids 而不是字符串作为输入

create procedure GetPersons @pageNumber int = 1, @pageSize int = 20, @orderBy tinyint = 1, @orderDir bit = 0
as

declare @orderByName varchar(50)
set @orderByName =  case @orderBy when 1 then 'id'
                        when 2 then 'firstName'
                        when 3 then 'lastName'
                    end 
                +' '+case @orderDir 
                        when 0 then 'desc' 
                        else 'asc' 
                    end

if @orderByName is null
begin
    raiserror('Cheater!', 16,1)
    return
end

declare @sql varchar(max)
set @sql = 'select id, firstName, lastName
from (
    select id, firstName, LastName, row_number() over(order by '+@orderByName+') as rn
    from Persons
    ) t
where rn > ('+cast(@pageNumber as varchar)+'-1) * '+cast(@pageSize as varchar)+'
        and rn <= '+cast(@pageNumber as varchar)+' * '+cast(@pageSize as varchar)+' 
order by '+@orderByName

exec(@sql)
Run Code Online (Sandbox Code Playgroud)

还有其他建议吗?

AMt*_*two 11

在您的示例代码中,您将三类“事物”传递到动态 SQL 中。

  1. 您传递@OrderDir,这是一个表示ASC或的关键字DESC
  2. 您传递@OrderBy,它是一个列名(或可能是一组列名,但根据#1 的实现方式,我假设您期望一个列名。
  3. 您传递@PageNumberand @PageSize,它成为生成的字符串中的文字。

关键词

这真的很简单——您只想验证您的输入。您很清楚这是该选项的正确选择。在这种情况下,您期望ASCDESC,因此您可以检查用户是否传递了这些值之一,或者您可以切换到不同的参数语义,其中您有一个参数是切换开关。将您的参数声明为@SortAscending bit = 0,然后在您的存储过程中,将该位转换为ASCDESC

列名

在这里,您应该使用该QUOTENAME功能。Quotename 将确保对象得到正确 [quoted],确保如果有人试图传入“; TRUNCATE TABLE USERS”的“列”,它将被视为列名,而不是任意一段注入代码。这将失败,而不是截断USERS表:

SELECT [; TRUNCATE TABLE USERS]...
FROM...
Run Code Online (Sandbox Code Playgroud)

文字和参数

对于@PageNumberand @PageSize,您应该使用它sp_executesql来正确传递参数。正确参数化动态SQL可以让你不仅传递价值,而且还得到值回

在此示例中,@x@y将是作用域为您的存储过程的变量。它们在您的动态 SQL 中不可用,因此您将它们传递到@a@b,它们的范围限定为动态 SQL。这允许您在动态 SQL 内部和外部拥有正确类型的值。

DECLARE @i int,
 @x int,
 @y int,
 @sql nvarchar(1000),
 @params nvarchar(1000);


SET @x = 10;
SET @y = 5;
SET @params = N'@i_out int OUT, @a int, @b int';
SET @sql    = N'SELECT @i_out = @a + @b';


EXEC sp_executesql @sql, @params, @i_out = @i OUT, @a = @x, @b = @y; 
SELECT @i;
Run Code Online (Sandbox Code Playgroud)

即使使用 varchar 值,将值保留为变量也可以防止有人随意传递要执行的代码。此示例确保用户输入得到SELECTed,而不是任意执行:

DECLARE @UserInput varchar(100),
         @params nvarchar(1000) = N'@value varchar(100)',
         @sql nvarchar(1000)    = N'SELECT Value = @value';

SET @UserInput = '; TRUNCATE TABLE USERS;'
EXEC sp_executesql @sql, @params, @value = @UserInput;  
Run Code Online (Sandbox Code Playgroud)

我的代码

这是我的存储过程版本,带有表定义和一些示例行:

CREATE TABLE dbo.Persons
       (
        id INT,
        firstName VARCHAR(50),
        lastName VARCHAR(50)
       );
GO

INSERT INTO dbo.Persons(id, firstName,lastName)
VALUES (1,'George','Washington'),
       (2,'John','Adams'),
       (3,'Thomas','Jefferson'),
       (4,'James','Madison'),
       (5,'James','Monroe')


ALTER PROCEDURE dbo.GetPersons
       @pageNumber INT = 1,
       @pageSize INT = 20,
       @orderBy VARCHAR(50) = 'id',
       @orderDir VARCHAR(4) = 'desc'
AS
       SET NOCOUNT ON;

--validate inputs
IF NOT EXISTS ( SELECT   1 FROM     sys.columns
                WHERE    object_id = OBJECT_ID('dbo.Persons')
                AND name = @orderBy )
BEGIN
        RAISERROR('Order by column does not exist.', 16,1);
        RETURN;
END;

IF (@orderDir NOT IN ('ASC', 'DESC'))
BEGIN
        RAISERROR('Order direction is invalid. Must be ASC or DESC.', 16,1);
        RETURN;
END;

--Now do stuff
--sp_executesql takes in nvarchar as a datatype
DECLARE @sql NVARCHAR(MAX);

SET @sql = N'SELECT id, firstName, lastName
FROM (
    SELECT id, firstName, LastName, ROW_NUMBER() OVER(ORDER BY '
           + QUOTENAME(@orderBy) + N' ' + @orderDir + N') AS rn
    FROM dbo.Persons
    ) t
WHERE rn > ( @pageNumber-1) * @pageSize
        AND rn <= @pageNumber * @pageSize 
ORDER BY ' + QUOTENAME(@orderBy) + N' ' + @orderDir;

EXEC sys.sp_executesql @sql, N'@pageNumber int, @pageSize int',
                   @pageNumber = @pageNumber, @pageSize = @pageSize;

GO
Run Code Online (Sandbox Code Playgroud)

你可以在这里看到,代码是功能性的,并为你提供了正确的排序和分页:

EXEC dbo.GetPersons @OrderBy = 'id', @orderDir = 'DESC';
EXEC dbo.GetPersons @OrderBy = 'id', @orderDir = 'ASC';
EXEC dbo.GetPersons @OrderBy = 'firstName';
EXEC dbo.GetPersons @OrderBy = 'lastName';
EXEC dbo.GetPersons @PageNumber = 2, @PageSize = 1, @OrderBy = 'lastName', @orderDir = 'ASC';
Run Code Online (Sandbox Code Playgroud)

还要看看输入处理如何防止有人试图做奇怪的事情:

EXEC dbo.GetPersons @OrderBy = 'lastName', @orderDir = 'UP';
EXEC dbo.GetPersons @OrderBy = ';TRUNCATE TABLE Persons;';
Run Code Online (Sandbox Code Playgroud)

补充阅读

sp_executesql 示例

Aaron Bertrand 的坏习惯:使用 EXEC() 而不是 sp_executesql

Aaron Bertrand 的厨房水槽程序