Einschleusung von SQL-Befehlen

Gilt für:SQL ServerAzure SQL-DatenbankAzure SQL Managed InstanceAzure Synapse AnalyticsAnalytics Platform System (PDW)

Eine Einschleusung von SQL-Befehlen ist ein Angriff, bei dem Schadcode in Zeichenfolgen eingefügt wird, die später zur Analyse und Ausführung an eine Instanz von SQL Server übergeben werden. Sie sollten jede Prozedur, die SQL-Anweisungen erstellt, nach Injection-Anfälligkeiten überprüfen, da die Datenbank-Engine alle empfangenen syntaktisch gültigen Abfragen ausführt. Auch parametrisierte Daten können von einem technisch versierten Angreifer manipuliert werden.

Funktionsweise von SQL Injection

Die Primärform von SQL-Injection besteht aus dem direkten Einfügen des Codes in Benutzereingabevariablen, die mit SQL-Befehlen verkettet und ausgeführt werden. Bei einem weniger direkten Angriff wird bösartiger Code in Zeichenfolgen eingefügt, die in einer Tabelle oder als Metadaten gespeichert werden sollen. Wenn die gespeicherten Zeichenfolgen dann in einem dynamischen SQL-Befehl verkettet werden, wird der bösartige Code ausgeführt.

Die Funktionalität des Injection-Prozesses besteht in der vorzeitigen Beendigung einer Textzeichenfolge und dem Anhängen eines neuen Befehls. Da vor der Ausführung des eingefügten Befehls möglicherweise weitere Zeichenfolgen daran angehängt wurden, beendet der Angreifer die eingefügte Zeichenfolge durch eine ---Kommentarmarkierung. Nachfolgender Text wird zur Ausführungszeit ignoriert.

Das folgende Skript zeigt ein einfaches SQL-Injection-Beispiel. Das Skript erstellt eine SQL-Abfrage, indem hartcodierte Zeichenfolgen mit einer vom Benutzer eingegebenen Zeichenfolge verkettet werden:

var ShipCity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

Der Benutzer wird aufgefordert, den Namen einer Stadt einzugeben. Bei der Eingabe von Redmond sieht die vom Skript erstellte Abfrage ungefähr wie folgt aus:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

Angenommen, der Benutzer gibt jedoch folgenden Text ein:

Redmond';drop table OrdersTable--

In diesem Fall wird die folgende Abfrage vom Skript erstellt:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'

Das Semikolon (;) bezeichnet das Ende der einen Abfrage und den Start einer anderen. Der doppelte Bindestrich (--) zeigt an, dass es sich beim restlichen Teil der aktuellen Zeile um einen Kommentar handelt, und die Zeile sollte ignoriert werden. Wenn der geänderte Code syntaktisch richtig ist, wird er vom Server ausgeführt. Wenn die Datenbank-Engine diese Anweisung verarbeitet, werden zuerst alle Datensätze in OrdersTable ausgewählt, wobei ShipCityRedmond ist. Die Datenbank-Engine löscht dann OrdersTable.

Solange der eingefügte SQL-Code syntaktisch richtig ist, kann eine Manipulation nicht programmgesteuert erkannt werden. Aus diesem Grund müssen Sie die Gültigkeit aller Benutzereingaben überprüfen und sorgfältig den Code überprüfen, der in dem von Ihnen verwendeten Server die erstellten SQL-Befehle ausführt. Die bewährten Methoden zum Schreiben von Code werden in den nachfolgenden Abschnitten in diesem Artikel beschrieben.

Überprüfen aller Eingaben

Überprüfen Sie immer alle Benutzereingaben, indem Sie Typ, Länge, Format und Bereich testen. Bei der Implementierung von Vorsichtsmaßnahmen gegen böswillige Eingaben sollten Sie die Architektur und die Bereitstellungsszenarien Ihrer Anwendung berücksichtigen. Denken Sie daran, dass Programme, die zur Ausführung in einer sicheren Umgebung entworfen wurden, in eine nicht sichere Umgebung kopiert werden können. Die folgenden Vorschläge sollten als bewährte Methoden angesehen werden:

  • Machen Sie keine Annahmen über Größe, Typ oder Inhalt der Daten, die von der Anwendung empfangen wurden. Beispielsweise sollten Sie die folgende Auswertung vornehmen:

    • Wie verhält sich Ihre Anwendung, wenn ein fehlgeleiteter oder böswilliger Benutzer eine 2 GB große Videodatei an einer Stelle eingibt, an der die Anwendung eine Postleitzahl erwartet?

    • Wie verhält sich die Anwendung bei einer eingebetteten DROP TABLE-Anweisung in einem Textfeld?

  • Testen Sie Größe und Datentyp der Eingaben, und erzwingen Sie entsprechende Grenzwerte. Dies kann bei der Vermeidung absichtlicher Pufferüberläufe hilfreich sein.

  • Testen Sie den Inhalt der Zeichenfolgenvariablen, und akzeptieren Sie nur erwartete Werte. Lehnen Sie Einträge mit Binärdaten, Escapesequenzen und Befehlszeichen ab. Dies kann bei der Verhinderung von Injection-Skripts hilfreich sein und schützt möglicherweise vor einigen Pufferüberlauf-Exploits.

  • Überprüfen Sie alle Daten bei der Eingabe gegen das Schema, wenn Sie mit XML-Dokumenten arbeiten.

  • Erstellen Sie keine Transact-SQL-Anweisungen direkt aus den Benutzereingaben.

  • Verwenden Sie gespeicherte Prozeduren, um Benutzereingaben zu überprüfen.

  • In Multi-Tier-Umgebungen sollten alle Daten vor dem Zugang zu einer vertrauensvollen Zone überprüft werden. Daten, die den Verifizierungsprozess nicht bestehen, sollten zurückgewiesen und eine Fehlermeldung sollte an die vorherige Stufe zurückgegeben werden.

  • Implementieren Sie mehrere Überprüfungsebenen. Vorsichtsmaßnahmen, die Sie gegen gelegentliche böswillige Benutzer einsetzen, sind möglicherweise gegen technisch versierte Angreifer unwirksam. Die bessere Methode besteht aus dem Überprüfen der Eingaben in die Benutzeroberfläche und aller nachfolgenden Punkte, an denen eine Vertrauensgrenze gekreuzt wird.

    Beispielsweise kann die Datenüberprüfung in einer clientseitigen Anwendung einfache Skript-Injection verhindern. Wenn jedoch auf der nächsten Ebene davon ausgegangen wird, dass die Eingabe bereits überprüft wurde, erhalten böswillige Benutzer mit der Fähigkeit, einen Client zu umgehen, uneingeschränkten Systemzugriff.

  • Verketten Sie niemals Benutzereingaben, die nicht überprüft wurden. Die Zeichenfolgenverkettung ist der primäre Eingangspunkt für Script-Injection.

  • Folgende Zeichenfolge dürfen in Feldern, aus denen Dateiname erstellt werden können, nicht zugelassen werden: AUX, CLOCK$, COM1 bis COM8, CON, CONFIG$, LPT1 bish LPT8, NUL und PRN.

Weisen Sie, wenn möglich, Eingaben mit den folgenden Zeichen zurück.

Eingabezeichen Bedeutung in Transact-SQL
; Abfragetrennzeichen
' Trennzeichen für Datenzeichenfolgen.
-- Einzeiliges Kommentartrennzeichen. Text hinter -- bis zum Ende dieser Zeile wird vom Server nicht ausgewertet.
/*** ... ***/ Kommentartrennzeichen. Text zwischen /* und */ wird vom Server nicht ausgewertet.
xp_ Wird am Anfang des Namens von erweiterten gespeicherten Katalogprozeduren verwendet, wie z.B. xp_cmdshell.

Verwenden von typsicheren SQL-Parametern

Die Parameters-Sammlung in der Datenbank-Engine stellt die Überprüfung von Typ und Länge bereit. Wenn Sie die Parameters-Sammlung verwenden, werden Eingaben als Literalwert und nicht als ausführbarer Code behandelt. Ein weiterer Vorteil beim Verwenden der Parameters-Sammlung besteht im Erzwingen von Typ- und Längenprüfungen. Bei Werten außerhalb des Bereichs wird eine Ausnahme ausgelöst. Das folgende Codefragment zeigt die Verwendung der Parameters-Sammlung:

SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

In diesem Beispiel wird der @au_id -Parameter als Literalwert und nicht als ausführbarer Code behandelt. Bei diesem Wert werden Typ und Länge überprüft. Wenn der Wert @au_id keiner der angegebenen Typ- und Längen-Constraints entspricht, wird eine Ausnahme zurückgegeben.

Verwenden von parametrisierten Eingaben mit gespeicherten Prozeduren

Wenn gespeicherte Prozeduren ungefilterte Eingaben verwenden, sind sie möglicherweise anfällig für SQL Injection. Der folgende Code ist z. B. nicht gegen Angriffe geschützt:

SqlDataAdapter myCommand =
    new SqlDataAdapter("LoginStoredProcedure '" + Login.Text + "'", conn);

Wenn Sie gespeicherte Prozeduren verwenden, sollten Sie Parameter als Eingaben verwenden.

Verwenden der Parameterauflistung mit Dynamic SQL

Wenn die Verwendung von gespeicherten Prozeduren nicht möglich ist, können Sie weiterhin Parameter verwenden, wie in dem nachfolgenden Codebeispiel gezeigt wird.

SqlDataAdapter myCommand = new SqlDataAdapter(
    "SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn);
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

Filtereingabe

Auch das Filtern von Eingaben kann sich beim Schutz vor SQL-Injection als hilfreich erweisen. Bei dieser Methode werden Escapezeichen entfernt. Die große Zeichenmenge kann aber möglicherweise Probleme verursachen, da Filterung keine verlässliche Verteidigungsmaßnahme ist. Das folgende Beispiel sucht nach Zeichenfolgentrennzeichen.

private string SafeSqlLiteral(string inputSQL)
{
    return inputSQL.Replace("'", "''");
}

LIKE-Klauseln

Beachten Sie beim Verwenden einer LIKE-Klausel, dass Platzhalterzeichen weiterhin mit Escapezeichen versehen werden müssen:

s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");

Überprüfen von Code gegen SQL-Injection

Sie sollten sämtlichen Code überprüfen, der EXECUTE, EXECoder sp_executesqlaufruft. Mithilfe von Abfragen, die den folgenden ähnlich sind, können Sie Prozeduren identifizieren, die diese Anweisungen enthalten. Diese Abfrage überprüft 1, 2, 3 oder 4 Leerzeichen nach den Wörtern EXECUTE und EXEC.

SELECT object_Name(id)
FROM syscomments
WHERE UPPER(TEXT) LIKE '%EXECUTE (%'
    OR UPPER(TEXT) LIKE '%EXECUTE  (%'
    OR UPPER(TEXT) LIKE '%EXECUTE   (%'
    OR UPPER(TEXT) LIKE '%EXECUTE    (%'
    OR UPPER(TEXT) LIKE '%EXEC (%'
    OR UPPER(TEXT) LIKE '%EXEC  (%'
    OR UPPER(TEXT) LIKE '%EXEC   (%'
    OR UPPER(TEXT) LIKE '%EXEC    (%'
    OR UPPER(TEXT) LIKE '%SP_EXECUTESQL%';

Einschließen von Parameter mit QUOTENAME() und REPLACE()

Überprüfen Sie für jede ausgewählte gespeicherte Prozedur, ob alle Variablen richtig behandelt werden, die in dynamischem Transact-SQL verwendet werden. Daten aus den Eingabeparametern der gespeicherten Prozedur oder Daten, die aus der Tabelle gelesen werden, sollten in QUOTENAME() oder REPLACE() eingeschlossen werden. Beachten Sie, dass der @variable-Wert, der an QUOTENAME() übergeben wird, zu sysname gehört. Er hat eine maximale Länge von 128 Zeichen.

@variable Empfohlener Wrapper
Der Name des sicherungsfähigen Elements QUOTENAME(@variable)
Zeichenfolge aus <= 128 Zeichen QUOTENAME(@variable, '''')
Zeichenfolge aus > 128 Zeichen REPLACE(@variable,'''', '''''')

Wenn Sie diese Technik verwenden, kann eine SET-Anweisung wie folgt überarbeitet werden:

-- Before:
SET @temp = N'SELECT * FROM authors WHERE au_lname ='''
    + @au_lname + N'''';

-- After:
SET @temp = N'SELECT * FROM authors WHERE au_lname = '''
    + REPLACE(@au_lname, '''', '''''') + N'''';

Aktivieren von SQL-Injection durch das Abschneiden von Daten

Jede dynamische Transact–SQL-Anweisung, die einer Variablen zugewiesen ist, wird abgeschnitten, falls sie größer als der für die Variable zugewiesene Puffer ist. Ein Angreifer, der in der Lage ist, das Abschneiden der Anweisungen durch das Übergeben von unerwartet langen Zeichenfolgen an eine gespeicherte Prozedur zu erzwingen, kann das Ergebnis verändern. Beispielsweise ist die gespeicherte Prozedur, die von dem folgenden Skript erstellt wurde, anfällig für SQL-Injection durch aktiviertes Abschneiden.

In diesem Beispiel haben wir einen @command-Puffer mit einer maximalen Länge von 200 Zeichen. Wir benötigen insgesamt 154 Zeichen, um das Kennwort von 'sa' festzulegen: 26 für UPDATE, 16 für die WHERE-Klausel, 4 für 'sa' und 2 für Anführungszeichen, umgeben von QUOTENAME(@loginname): 200 - 26 - 16 - 4 - 2 = 154. Da @new jedoch als Sysname deklariert wird, kann diese Variable nur 128 Zeichen enthalten. Dies kann umgangen werden, indem einige einfache Anführungszeichen in @new übergeben werden.

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variable.
DECLARE @command VARCHAR(200)

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password=' + QUOTENAME(@new, '''')
    + ' WHERE username=' + QUOTENAME(@loginname, '''') 
    + ' AND password=' + QUOTENAME(@old, '''')

-- Execute the command.
EXEC (@command);
GO

Durch das Übergeben von 154 Zeichen an einen Puffer aus 128 Zeichen kann ein Angreifer ein neues Kennwort für sa festlegen, ohne das alte Kennwort zu kennen.

EXEC sp_MySetPassword 'sa',
    'dummy',
    '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''

Aus diesem Grund sollten Sie einen großen Puffer für eine Befehlsvariable verwenden oder dynamisches Transact-SQL direkt in der EXECUTE-Anweisung ausführen.

Abschneiden beim Verwenden von QUOTENAME(@variable, '''') und REPLACE()

Zeichenfolgen, die von QUOTENAME() und REPLACE() zurückgegeben werden, werden automatisch abgeschnitten, falls Sie über dem zugewiesenen Speicherplatz liegen. Die im folgenden Beispiel erstellte, gespeicherte Prozedur zeigt, was in den Fällen passieren kann.

In diesem Beispiel werden die in temporären Variablen gespeicherten Daten abgeschnitten, da die Puffergröße von @login, @oldpassword und @newpassword nur 128 Zeichen beträgt, QUOTENAME() jedoch bis zu 258 Zeichen zurückgeben kann. Wenn @new 128 Zeichen enthält, kann @newpassword123... n sein, wobei n das 127. Zeichen ist. Da die von QUOTENAME() zurückgegebene Zeichenfolge abgeschnitten wird, kann sie wie die folgende Anweisung aussehen:

UPDATE Users SET password ='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = QUOTENAME(@loginname, '''');
SET @oldpassword = QUOTENAME(@old, '''');
SET @newpassword = QUOTENAME(@new, '''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users set password = ' + @newpassword
    + ' WHERE username = ' + @login
    + ' AND password = ' + @oldpassword;

-- Execute the command.
EXEC (@command);
GO

Daher legt die folgende Anweisung die Kennwörter von allen Benutzern auf den Wert fest, der im vorherigen Code übergeben wurde.

EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'

Sie können das Abschneiden der Zeichenfolge durch das Überschreiten des zugewiesenen Pufferplatzes erzwingen, falls Sie REPLACE() verwenden. Die im folgenden Beispiel erstellte, gespeicherte Prozedur zeigt, was in den Fällen passieren kann.

In diesem Beispiel werden Daten abgeschnitten, da die für @login, @oldpassword und @newpassword zugewiesenen Puffer nur 128 Zeichen umfassen können, QUOTENAME() jedoch bis zu 258 Zeichen zurückgeben kann. Wenn @new 128 Zeichen enthält, kann @newpassword'123...n' sein, wobei n das 127. Zeichen ist. Da die von QUOTENAME() zurückgegebene Zeichenfolge abgeschnitten wird, kann sie wie die folgende Anweisung aussehen:

UPDATE Users SET password='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = REPLACE(@loginname, '''', '''''');
SET @oldpassword = REPLACE(@old, '''', '''''');
SET @newpassword = REPLACE(@new, '''', '''''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password = '''
    + @newpassword + ''' WHERE username = '''
    + @login + ''' AND password = ''' + @oldpassword + '''';

-- Execute the command.
EXEC (@command);
GO

Wie bei QUOTENAME() kann das Abschneiden der Zeichenfolge durch REPLACE() vermieden werden, indem temporäre Variablen deklariert werden, die für alle Fälle groß genug sind. Wenn möglich, sollten Sie QUOTENAME() oder REPLACE() direkt in der dynamischen Transact-SQL-Datei aufrufen. Andernfalls können Sie den erforderlichen Puffer wie folgt berechnen. Für @outbuffer = QUOTENAME(@input)sollte die Größe von @outbuffer gleich dem Wert 2 * (len(@input) + 1)sein. Wenn Sie wie in dem vorherigen Beispiel REPLACE() und doppelte Anführungszeichen verwenden, reicht ein Puffer von 2 * len(@input) aus.

Die folgende Berechnung deckt alle Fälle ab:

WHILE LEN(@find_string) > 0, required buffer size =
    ROUND(LEN(@input) / LEN(@find_string), 0)
        * LEN(@new_string) + (LEN(@input) % LEN(@find_string))

Abschneiden beim Verwenden von QUOTENAME(@variable, ']')

Ein Abschneiden ist möglich, wenn der Name eines sicherungsfähigen Elements an Anweisungen übergeben wird, die das Format QUOTENAME(@variable, ']') verwenden. Das folgende Beispiel veranschaulicht dieses Szenario.

In diesem Beispiel muss @objectname 2 * 258 + 1 Zeichen zulassen.

CREATE PROCEDURE sp_MyProc
    @schemaname SYSNAME,
    @tablename SYSNAME
AS
-- Declare a variable as sysname. The variable will be 128 characters.
DECLARE @objectname SYSNAME;

SET @objectname = QUOTENAME(@schemaname) + '.' + QUOTENAME(@tablename);
    -- Do some operations.
GO

Wenn Sie Werte vom Typ sysname verketten, sollten Sie temporäre Variablen verwenden, die groß genug sind, um die maximale Anzahl von 128 Zeichen pro Wert aufzunehmen. Falls möglich, sollten Sie QUOTENAME() direkt innerhalb der dynamischen Transact-SQL aufrufen. Andernfalls können Sie die erforderliche Puffergröße so berechnen, wie es im vorherigen Abschnitt beschrieben wird.