Vamos imaginar um procedimento armazenado que recupera dados e faz algum tipo de paginação. Este procedimento tem algumas entradas que descrevem qual conjunto de dados queremos e como o classificamos.
Aqui está uma consulta muito simples, mas vamos tomá-la como exemplo.
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)
Deve ser usado assim:
exec GetPersons @pageNumber = 1, @pageSize = 20, @orderBy = 'id', @orderDir = 'desc'
Mas um cara esperto poderia lançar:
exec GetPersons @pageNumber = 1, @pageSize = 20, @orderBy = 'id)a from Persons)t;delete from Persons;print''', @orderDir = ''
... e soltar dados
Isso obviamente não é uma situação segura. E como poderíamos evitar?
Nota : esta pergunta não é sobre "é uma boa maneira de fazer paginação?" nem "é uma coisa boa fazer sql dinâmico?". A questão é sobre evitar a injeção de código ao construir consultas sql dinamicamente para ter algumas diretrizes para tornar o código um pouco mais limpo se tivermos que fazer procedimentos armazenados semelhantes novamente no futuro.
Algumas ideias básicas:
Validar entradas
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)
Passe ids em vez de strings como entradas
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)
Alguma outra sugestão?
Em seu código de exemplo, você está passando três categorias de "coisas" para seu SQL dinâmico.
ASC
ouDESC
.@PageNumber
e@PageSize
, que se tornam literais na string gerada.Palavras-chave
Isso é realmente simples - você só quer validar sua entrada. Você está certo de que esta é a coisa certa para esta opção. Nesse caso, você está esperando
ASC
ouDESC
, portanto, você pode verificar se o usuário passa um desses valores ou alternar para uma semântica de parâmetro diferente, onde você tem um parâmetro que é uma opção de alternância. Declare seu parâmetro como@SortAscending bit = 0
, em seguida, dentro de seu procedimento armazenado, traduza o bit paraASC
ouDESC
.Nomes de coluna
Aqui, você deve usar a
QUOTENAME
função. Quotename garantirá que os objetos sejam corretamente [quoted], garantindo que, se alguém tentar passar uma "coluna" de "; TRUNCATE TABLE USERS", ela será tratada como um nome de coluna e não como um pedaço arbitrário de código injetado. Isso falhará, em vez de truncar aUSERS
tabela:Literais e parâmetros
Para
@PageNumber
e@PageSize
, você deve usarsp_executesql
para passar parâmetros corretamente. Parametrizar corretamente seu SQL dinâmico permite não apenas passar valores, mas também obter valores de volta .Neste exemplo,
@x
e@y
seriam variáveis com escopo para seus procedimentos armazenados. Eles não estão disponíveis em seu SQL dinâmico, portanto, você os passa para@a
e@b
, que têm como escopo o SQL dinâmico. Isso permite que você tenha valores digitados corretamente dentro e fora do SQL dinâmico.Mesmo com valores varchar, manter o valor como uma variável impede que alguém passe arbitrariamente o código que é executado. Este exemplo garante que a entrada do usuário seja
SELECT
ed e não executada arbitrariamente:Meu código
Aqui está minha versão do seu procedimento armazenado, com definição de tabela e algumas linhas de amostra:
Você pode ver aqui, que o código é funcional e fornece a ordenação e paginação adequadas:
E veja também como a manipulação de entrada protege contra alguém tentando fazer coisas estranhas:
Leitura Adicional
exemplo sp_executesql
Maus Hábitos de Aaron Bertrand para Chutar: Usando EXEC() em vez de sp_executesql
Procedimento da pia da cozinha de Aaron Bertrand
Um método comum para mitigar a injeção de SQL é usar
QUOTENAME
em torno de variáveis que são passadas para o procedimento armazenado.Então, no seu exemplo, o código poderia ser modificado assim:
Se alguém tentasse passar o comando extra 'delete', a execução apresentaria um erro porque o SQL dinâmico resultante se parece com isso:
resultando neste erro:
Além disso, Aaron Bertrand tem um ótimo blog sobre Bad Habits to Kick : Using EXEC() em vez de sp_executesql
Uma solução óbvia é não usar SQL dinâmico. Acho que sua tarefa pode ser realizada com código T-SQL regular e não dinâmico, que também oferece outras vantagens em termos de segurança (como encadeamento de propriedade).
Então, em vez de:
Você poderia, por exemplo..
Leia-se,
OFFSET FETCH
A resposta de @Scott Hodgin toca nisso, mas basicamente a melhor abordagem ao gerar sequências SQL dinâmicas voltadas para o cliente/aplicativo é utilizar sp_executesql .
Embora não seja totalmente infalível para eliminar ataques de SQL Injection, sp_executesql é provavelmente o melhor que você obterá. O artigo que Scott linka por Aaron Bertrand é bastante direto, mas para resumir rapidamente os benefícios do sp_executesql sobre outras abordagens é que:
O primeiro ponto é o que eu sinto que é o mais importante relacionado à sua pergunta, porque você pode limitar o tamanho, o tipo, etc. dos seus parâmetros. Isso torna incrivelmente mais difícil injetar código desagradável.
Para fornecer uma resposta ainda mais completa, atualizei seu sp de acordo. Interessante o suficiente, no seu caso, porque você está tentando parametrizar literais de coluna, precisará aninhar instruções sp_executesql, para que a primeira instrução aninhada defina nomes de coluna como literais e a segunda execução passe os valores de paginação, da seguinte maneira:
Opção simples - junte-se a
sys.columns
para certificar-se de que é um nome de coluna válido e useCASE
como padrãoASC
se algo diferenteDESC
for passado.(oh, e use
nvarchar(max)
para@sql
esp_executesql
)