it-swarm.com.de

Verhindern der SQL-Injektion in dynamischem SQL

Stellen wir uns eine gespeicherte Prozedur vor, die Daten abruft und eine Art Paginierung durchführt. Diese Prozedur enthält einige Eingaben, die beschreiben, welchen Datensatz wir möchten und wie wir ihn sortieren.

Hier ist eine sehr einfache Abfrage, aber nehmen wir sie als Beispiel.

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 '[email protected]derBy+' '[email protected]+') 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 '[email protected]+' '[email protected]

exec(@sql)

Es soll so verwendet werden:

exec GetPersons @pageNumber = 1, @pageSize = 20, @orderBy = 'id', @orderDir = 'desc'

Aber ein kluger Kerl könnte starten:

exec GetPersons @pageNumber = 1, @pageSize = 20, @orderBy = 'id)a from Persons)t;delete from Persons;print''', @orderDir = ''

... und Daten löschen

Das ist offensichtlich keine sichere Situation. Und wie könnten wir das verhindern?

Anmerkung: Bei dieser Frage geht es nicht um "Ist es eine gute Möglichkeit, Paginierung zu machen?" noch "ist es eine gute Sache, dynamisches SQL zu machen?". Die Frage ist, wie die Code-Injection beim dynamischen Erstellen von SQL-Abfragen verhindert werden kann, um einige Richtlinien zu erhalten, die den Code ein bisschen sauberer machen, wenn wir in Zukunft ähnliche gespeicherte Prozeduren erneut ausführen müssen.

Einige Grundideen:

Eingaben validieren

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 '[email protected]+' '[email protected]+') 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 '[email protected]+' '[email protected]

exec(@sql)

Übergeben Sie IDs anstelle von Zeichenfolgen als Eingaben

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 '[email protected]+') 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 '[email protected]

exec(@sql)

Irgendwelche anderen Vorschläge?

6
irimias

In Ihrem Beispielcode übergeben Sie drei Kategorien von "Dingen" an Ihr dynamisches SQL.

  1. Sie übergeben @OrderDir, ein Schlüsselwort für ASC oder DESC.
  2. Sie übergeben @OrderBy, einen Spaltennamen (oder möglicherweise eine Reihe von Spaltennamen, aber basierend auf der Art und Weise, wie # 1 implementiert wird, gehe ich davon aus, dass Sie einen einzelnen Spaltennamen erwarten.
  3. Sie übergeben @PageNumber Und @PageSize, Die in der generierten Zeichenfolge zu Literalen werden.

Schlüsselwörter

Dies ist wirklich einfach - Sie möchten nur Ihre Eingabe validieren. Sie sind sich sicher, dass dies das Richtige für diese Option ist. In diesem Fall erwarten Sie entweder ASC oder DESC, sodass Sie entweder überprüfen können, ob der Benutzer einen dieser Werte übergibt, oder zu einer anderen Parametersemantik wechseln können, in der Sie eine haben Parameter, der ein Kippschalter ist. Deklarieren Sie Ihren Parameter als @SortAscending bit = 0 Und übersetzen Sie das Bit dann in Ihrer gespeicherten Prozedur entweder in ASC oder DESC.

Spaltennamen

Hier sollten Sie die Funktion QUOTENAME verwenden. Quotename stellt sicher, dass Objekte ordnungsgemäß [in Anführungszeichen] gesetzt werden. Wenn jemand versucht, eine "Spalte" von "; TRUNCATE TABLE USERS" zu übergeben, wird dies als Spaltenname und nicht als beliebiger Teil des injizierten Codes behandelt. Dies schlägt fehl, anstatt die Tabelle USERS abzuschneiden:

SELECT [; TRUNCATE TABLE USERS]...
FROM...

Literale & Parameter

Für @PageNumber Und @PageSize Sollten Sie sp_executesql Verwenden, um die Parameter ordnungsgemäß zu übergeben. Durch die richtige Parametrisierung Ihres dynamischen SQL können Sie nicht nur Werte übergeben, sondern auch Werte zurückgeben .

In diesem Beispiel sind @x Und @y Variablen für Ihre gespeicherten Prozeduren. Sie sind in Ihrem dynamischen SQL nicht verfügbar, daher übergeben Sie sie an @a Und @b, Die für das dynamische SQL gelten. Auf diese Weise können Sie Werte sowohl innerhalb als auch außerhalb des dynamischen SQL korrekt eingeben.

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;

Selbst bei varchar-Werten verhindert das Beibehalten des Werts als Variable, dass jemand willkürlich Code ausgibt, der ausgeführt wird. Dieses Beispiel stellt sicher, dass die Benutzereingabe SELECTed wird und nicht willkürlich ausgeführt wird:

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;  

Mein Code

Hier ist meine Version Ihrer gespeicherten Prozedur mit Tabellendefinition und einigen Beispielzeilen:

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

Sie können hier sehen, dass der Code funktionsfähig ist und Ihnen die richtige Reihenfolge und Paginierung bietet:

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';

Und sehen Sie auch, wie die Eingabebehandlung vor jemandem schützt, der versucht, seltsame Dinge zu tun:

EXEC dbo.GetPersons @OrderBy = 'lastName', @orderDir = 'UP';
EXEC dbo.GetPersons @OrderBy = ';TRUNCATE TABLE Persons;';

Zusätzliche Lektüre

sp_executesql Beispiel

Aaron Bertrands schlechte Angewohnheiten zum Treten: Verwenden von EXEC () anstelle von sp_executesql

Aaron Bertrands Spülbeckenverfahren

9
AMtwo

Eine übliche Methode zur Reduzierung der SQL-Injection ist die Verwendung von QUOTENAME um Variablen, die an die gespeicherte Prozedur übergeben werden.

In Ihrem Beispiel könnte der Code also folgendermaßen geändert werden:

declare @sql varchar(max)
set @sql = 'select id, firstName, lastName
from (
    select id, firstName, LastName, row_number() over(order by '[email protected]+' '[email protected]+') 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)+' '[email protected]

Wenn jemand versucht, den zusätzlichen Befehl 'delete' zu übergeben, wird die Ausführung fehlschlagen, da das resultierende dynamische SQL folgendermaßen aussieht:

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'] 

was zu diesem Fehler führt:

Nachricht 102, Ebene 15, Zustand 1, Zeile 27
Falsche Syntax in der Nähe ']'.

Außerdem hat Aaron Bertrand einen großartigen Blog über Bad Habits to Kick: Verwenden von EXEC () anstelle von sp_executesql

3
Scott Hodgin

Eine naheliegende Lösung besteht darin, kein dynamisches SQL zu verwenden. Ich denke, Ihre Aufgabe kann mit normalem, nicht dynamischem T-SQL-Code erledigt werden, der Ihnen auch andere Vorteile in Bezug auf die Sicherheit bietet (wie die Verkettung von Eigentumsrechten).

Also statt:

declare @sql varchar(max)
set @sql = 'select id, firstName, lastName
from (
    select id, firstName, LastName, row_number() over(order by '[email protected]+') 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 '[email protected]

exec(@sql)

Sie könnten zum Beispiel ..

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;

Lesen Sie weiter,

2

Die Antwort von @Scott Hodgin berührt dies, aber im Grunde ist die Verwendung von sp_executesql der beste Ansatz beim Generieren von dynamischen SQL-Zeichenfolgen, die für Clients/Apps verfügbar sind.

Sp_executesql ist zwar nicht ganz kinderleicht, um SQL Injection-Angriffe zu eliminieren, aber wahrscheinlich das Beste, was Sie bekommen werden. Der Artikel Scott-Link von Aaron Bertrand ist ziemlich einfach, aber um die Vorteile von sp_executesql gegenüber anderen Ansätzen schnell zusammenzufassen, gilt Folgendes:

  1. Verwendet stark typisierte Variablen innerhalb der Zeichenfolge
  2. Hat eine bessere Chance, den Abfrageplan wiederzuverwenden

Der erste Punkt ist meiner Meinung nach der wichtigste im Zusammenhang mit Ihrer Frage, da Sie die Länge, den Typ usw. Ihrer Parameter einschränken können. Dies macht es unglaublich schwieriger, bösen Code einzufügen.

Um eine noch vollständigere Antwort zu erhalten, habe ich Ihre SP entsprechend aktualisiert. Interessanterweise müssen Sie in Ihrem Fall, da Sie versuchen, Spaltenliterale zu parametrisieren, sp_executesql-Anweisungen verschachteln, sodass die erste verschachtelte Anweisung Spaltennamen als Literale festlegt und die zweite Ausführung die Paginierungswerte wie folgt übergibt:

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', @[email protected], @[email protected], @[email protected]_out OUTPUT
EXEC sp_executesql @sql_out, N'@pN int, @pS int', @[email protected], @[email protected]
0
John Eisbrener

Einfache Option - Verbinden Sie sich mit sys.columns, Um sicherzustellen, dass es sich um einen gültigen Spaltennamen handelt, und verwenden Sie CASE, um standardmäßig ASC zu verwenden, wenn etwas anderes als DESC übergeben wird .

(oh, und benutze nvarchar(max) für @sql und 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;
0
Rob Farley