Imaginons une procédure stockée qui récupère des données et effectue une sorte de pagination. Cette procédure contient des entrées décrivant quel ensemble de données nous voulons et comment nous le trions.
Voici une requête très simple, mais prenons-la comme exemple.
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)
Il est censé être utilisé comme ça:
exec GetPersons @pageNumber = 1, @pageSize = 20, @orderBy = 'id', @orderDir = 'desc'
Mais un gars intelligent pourrait lancer:
exec GetPersons @pageNumber = 1, @pageSize = 20, @orderBy = 'id)a from Persons)t;delete from Persons;print''', @orderDir = ''
... et déposer des données
Ce n'est évidemment pas une situation sûre. Et comment pourrions-nous l'empêcher?
Note: cette question ne concerne pas "est-ce un bon moyen de faire la pagination?" ni "est-ce une bonne chose de faire du sql dynamique?". La question est d'empêcher l'injection de code lors de la construction dynamique des requêtes SQL afin d'avoir des directives pour rendre le code un peu plus propre si nous devons refaire des procédures stockées similaires à l'avenir.
Quelques idées de base:
Valider les entrées
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)
Passez les identifiants au lieu des chaînes comme entrées
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)
D'autres suggestions?
Dans votre exemple de code, vous passez trois catégories de "choses" dans votre SQL dynamique.
ASC
ou DESC
.@PageNumber
et @PageSize
, qui deviennent des littéraux dans la chaîne générée.C'est vraiment simple - vous voulez juste valider votre entrée. Vous savez parfaitement que c'est la bonne chose pour cette option. Dans ce cas, vous vous attendez à ASC
ou DESC
, vous pouvez donc soit vérifier que l'utilisateur transmet l'une de ces valeurs, soit basculer vers un autre paramètre sémantique, où vous avez un paramètre qui est un interrupteur à bascule. Déclarez votre paramètre comme @SortAscending bit = 0
, puis dans votre procédure stockée, traduisez le bit en ASC
ou DESC
.
Ici, vous devez utiliser la fonction QUOTENAME
. Quotename s'assurera que les objets sont correctement [entre guillemets], garantissant que si quelqu'un essaie de passer dans une "colonne" de "; TRUNCATE TABLE USERS", il sera traité comme un nom de colonne, et non comme un morceau arbitraire de code injecté. Cela échouera, plutôt que de tronquer la table USERS
:
SELECT [; TRUNCATE TABLE USERS]...
FROM...
Pour @PageNumber
et @PageSize
, vous devez utiliser sp_executesql
pour passer correctement les paramètres. Le paramétrage correct de votre SQL dynamique vous permet non seulement de transmettre des valeurs, mais également de récupérer des valeurs .
Dans cet exemple, @x
et @y
serait des variables étendues à vos procédures stockées. Ils ne sont pas disponibles dans votre SQL dynamique, vous les transmettez donc à @a
et @b
, qui sont limités au SQL dynamique. Cela vous permet d'avoir des valeurs correctement saisies à l'intérieur et à l'extérieur du SQL dynamique.
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;
Même avec des valeurs varchar, conserver la valeur en tant que variable empêche quelqu'un de passer arbitrairement du code qui est exécuté. Cet exemple garantit que l'entrée utilisateur est SELECT
ed et n'est pas exécutée arbitrairement:
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;
Voici ma version de votre procédure stockée, avec la définition de la table et quelques exemples de lignes:
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
Vous pouvez voir ici que le code est fonctionnel et vous donne l'ordre et la pagination appropriés:
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';
Et voyez également comment la gestion des entrées protège contre quelqu'un qui essaie de faire des choses étranges:
EXEC dbo.GetPersons @OrderBy = 'lastName', @orderDir = 'UP';
EXEC dbo.GetPersons @OrderBy = ';TRUNCATE TABLE Persons;';
Les mauvaises habitudes d'Aaron Bertrand: Kick: Utiliser EXEC () au lieu de sp_executesql
Une méthode courante pour atténuer l'injection SQL consiste à utiliser QUOTENAME
autour des variables transmises à la procédure stockée.
Donc, dans votre exemple, le code pourrait être modifié comme ceci:
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 '+ Quotename(@orderBy)+' '+@orderDir
Si quelqu'un essayait de passer la commande supplémentaire "supprimer", l'exécution serait une erreur car le SQL dynamique résultant ressemble à ceci:
select id, firstName, lastName
from (
select id, firstName, LastName, row_number() over(order by id)a from Persons)t;delete from Persons;print' ) as rn
from Persons
) t
where rn > (1-1) * 20
and rn <= 1 * 20
order by [id)a from Persons)t;delete from Persons;print']
entraînant cette erreur:
Msg 102, niveau 15, état 1, ligne 27
Syntaxe incorrecte près de ']'.
En outre, Aaron Bertrand a un excellent blog sur Bad Habits to Kick: Utilisation d'EXEC () au lieu de sp_executesql
Une solution évidente est de ne pas utiliser de SQL dynamique. Je pense que votre tâche peut être accomplie avec du code T-SQL régulier et non dynamique, ce qui vous donne également d'autres avantages en termes de sécurité (comme le chaînage de la propriété).
Donc au lieu de:
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)
Vous pourriez, par exemple ..
SELECT id, firstName, lastName
FROM (
SELECT id, firstName, lastName, ROW_NUMBER() OVER (
ORDER BY (CASE @OrderByName
WHEN 1 THEN id END),
--- different datatypes, I'm assuming
(CASE
WHEN 2 THEN firstName
WHEN 3 THEN lastName END)) AS rn
FROM persons
) AS t
WHERE rn > (@pageNumber-1) * @pageSize
AND rn <= @pageNumber * @pageSize
ORDER BY rn;
Lire sur,
OFFSET FETCH
La réponse de @Scott Hodgin touche à cela, mais fondamentalement, la meilleure approche lors de la génération de chaînes SQL dynamiques face au client/à l'application consiste à utiliser sp_executesql .
Bien qu'il ne soit pas entièrement infaillible pour éliminer les attaques par injection SQL, sp_executesql est probablement le meilleur que vous obtiendrez. Le article liens vers Scott par Aaron Bertrand est assez simple, mais pour résumer rapidement les avantages de sp_executesql par rapport à d'autres approches, c'est qu'il:
Le premier point est ce qui me semble le plus important par rapport à votre question car vous pouvez limiter la longueur, le type, etc. de vos paramètres, ce qui rend incroyablement plus difficile l'injection de code désagréable.
Quant à fournir une réponse encore plus complète, j'ai mis à jour votre sp en conséquence. Assez intéressant, dans votre cas, car vous essayez de paramétrer des littéraux de colonne, vous devrez imbriquer les instructions sp_executesql, de sorte que la première instruction imbriquée définit les noms de colonne en tant que littéraux et la deuxième exécution passe dans les valeurs de pagination, comme suit:
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 nvarchar(max), @sql_out nvarchar(max)
set @sql = 'SELECT @sql_out = ''select id, firstName, lastName
from (
select id, firstName, LastName, row_number() over(order by '' + @oB + '' '' + @oD + '') as rn
from Persons
) t
where rn > (@pN -1) * @pS
and rn <= @pN * @pS
order by '' + @oB + '' '' + @oD + '''''
--PRINT(@sql)
EXEC sp_executesql @sql, N'@oB varchar(50), @oD varchar(4), @sql_out nvarchar(max) OUTPUT', @oB=@orderBy, @oD=@orderDir, @sql_out=@sql_out OUTPUT
EXEC sp_executesql @sql_out, N'@pN int, @pS int', @pN=@pageNumber, @pS=@pageSize
Option simple - joignez-vous à sys.columns
Pour vous assurer qu'il s'agit d'un nom de colonne valide et utilisez CASE
par défaut sur ASC
si autre chose que DESC
est passé .
(oh, et utilisez nvarchar(max)
pour @sql
et sp_executesql
)
declare @sql nvarchar(max)
select @sql = 'select id, firstName, lastName
from (
select id, firstName, LastName, row_number() over(order by '+QUOTENAME(c.name)+' '+ CASE WHEN @orderDir = 'DESC' THEN 'DESC' ELSE 'ASC' END +') 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 '+QUOTENAME(c.name)+' '+ CASE WHEN @orderDir = 'DESC' THEN 'DESC' ELSE 'ASC' END
FROM sys.columns c
WHERE c.name = @orderby
AND c.object_id = OBJECT_ID('dbo.Persons');
EXEC sp_executesql @sql;