Skip to main content

Einstieg in die Query-Parametrisierung

Artikel von
Headshot of Mary Gathoni

Mary Gathoni

blog feature toolkit

24. Oktober 2023

0 Min. Lesezeit

Wenn Webanwendungen auf von Nutzern bereitgestellte Daten angewiesen sind, besteht das potenzielle Risiko von SQL-Injection-Angriffen. SQL-Injection ist eine Technik, mit der eine SQL-Anweisung durch Manipulation von Nutzereingaben verändert wird. Angreifer senden dazu schädlichen SQL-Code an die Datenbank, um Sicherheitsmaßnahmen zu umgehen und sich unbefugten Zugriff auf Daten zu verschaffen. Die Folgen können schwerwiegend sein und von der Offenlegung vertraulicher Informationen bis zum vollständigen Verlust des Systems reichen.  

Es gibt verschiedene Möglichkeiten, einen SQL-Injection-Angriff zu verhindern. Eine wichtige Methode ist die SQL-Query-Parametrisierung. Dabei wird der SQL-Code mithilfe von Platzhaltern (Parametern) von den Werten der Nutzereingaben getrennt. So werden Eingaben als Daten und nicht als SQL-Code behandelt. Entscheidend ist, vorbereitete Anweisungen mit parametrisierten Queries zu verwenden – eine äußerst wirksame Kombination.

Für eine zusätzliche Sicherheitsebene können wir parametrisierte gespeicherte Prozeduren einsetzen. Gespeicherte Prozeduren sind vorkompilierte SQL-Anweisungen, die in der Datenbank abgelegt sind. Wir können die Berechtigung zur Ausführung der gespeicherten Prozedur erteilen und gleichzeitig den direkten Zugriff auf die zugrunde liegende Tabelle einschränken. Wenn wir eine Prozedur verwenden möchten, übergeben wir ihr die erwarteten Parameter aus unserer Anwendung.

In diesem praxisorientierten Artikel erfahren Sie, wie Sie SQL-Query-Parametrisierung und gespeicherte Prozeduren nutzen, um Injection-Angriffe zu verhindern, und welche zusätzlichen Sicherheitsmaßnahmen Ihren Code schützen.

Query-Parametrisierung implementieren

Für die Query-Parametrisierung werden Platzhalter in SQL-Queries verwendet, die die von Nutzern bereitgestellten Eingaben repräsentieren. Platzhalter sind temporäre Markierungen, die die SQL-Engine mit den tatsächlichen Variablenwerten füllt. Bei der Ausführung der Query ordnet die SQL-Engine die tatsächlichen Variablen den Platzhaltern zu, bevor sie die Query ausführt. So werden Variablen als literale Daten behandelt.

Sie können die Query-Parametrisierung mit oder ohne vorbereitete Anweisungen verwenden. Der Unterschied zwischen den beiden Ansätzen besteht darin, dass vorbereitete Anweisungen vorkompiliert sind und mehrfach mit verschiedenen Parametern aufgerufen werden können. 

Noch wichtiger: Vorbereitete Anweisungen können die Query-Parametrisierung verbessern, da die Datenbank sicherstellt, dass die Anweisung vorkompiliert wird. Das verkürzt die Ausführungszeit. Und da der Ausführungsplan bereits erstellt ist, beeinflussen Parameter den Ausführungsplan nicht – ein Vorteil für Ihre Sicherheitslage.

Wenn Sie eine Query mehrfach aufrufen müssen, braucht die Datenbank sie nicht erneut zu kompilieren. Sie können einfach die gewünschten Parameter an die vorbereitete Anweisung übergeben. Außerdem reduzieren gebundene Parameter den Bandbreitenverbrauch, da nur die Parameter und nicht die gesamte Query an den Server gesendet werden. 

In den folgenden Abschnitten erfahren Sie, wie Sie die Query-Parametrisierung in einer Webanwendung implementieren.

Voraussetzungen

Für die Teilnahme an dieser Demonstration benötigen Sie Folgendes:

Query-Parametrisierung ohne vorbereitete Anweisungen 

Zur Veranschaulichung arbeiten wir mit einer .NET-Konsolenanwendung, die Nutzereingaben über die Befehlszeile entgegennimmt und diese anschließend als einen der Werte in einer SQL-Query verwendet.

Sehen wir uns zunächst an, wie sich die Query ohne vorbereitete Anweisung parametrisieren lässt. 

Verwenden Sie den folgenden Code, um den Nutzernamen mit `Console.Readline()` abzurufen, und definieren Sie anschließend die Query. Anstatt den Nutzernamen an die Query anzuhängen, verwenden Sie einen mit dem Zeichen `@` markierten Platzhalter. Später binden wir den Nutzernamen mithilfe der Methode `AddWithValue()` an die Query, bevor wir sie ausführen:

try
{
    string? username = Console.ReadLine();
    string query = "INSERT INTO Users (pk_id, name) VALUES (@id, @username)";
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        SqlCommand command = new SqlCommand(query, connection);

        // Bind the SQL parameters.
        command.Parameters.AddWithValue("@username", username);
        command.Parameters.AddWithValue("@id", 20);

        // Execute the query
        command.ExecuteNonQuery();
        connection.Close();
    }

}
catch (Exception ex)
{
    //display error message
    Console.WriteLine("Exception: " + ex.Message);
}

Indem wir die Parameterwerte nach dem Definieren der Query übergeben, stellen wir sicher, dass der SQL Server diese Werte als Literale interpretiert.

Query-Parametrisierung mit vorbereiteten Anweisungen

Sehen wir uns im selben Szenario an, wie Sie die Such-Query so ändern, dass sie eine vorbereitete Anweisung verwendet.

Der Query-String bleibt unverändert und verwendet weiterhin die Platzhalter `@id` und `@username`. Anders als im vorherigen Beispiel rufen wir jedoch nach dem Festlegen der Parameter die Methode `Prepare` für das Objekt `SqlCommand` auf.

Geben Sie vor dem Aufruf von `Prepare()` den Datentyp jedes Parameters in der Anweisung an. Außerdem müssen Sie dessen Größeneigenschaft auf den Maximalwert festlegen, um einen Fehler zu vermeiden.

Verwenden Sie anschließend die Methode `ExecuteNonQuery`, um die `INSERT`-Anweisung mit den angegebenen Parametern auszuführen und die Nutzerdaten wie unten gezeigt in die Tabelle `Users` einzufügen:

try
{
    string? username = Console.ReadLine();

    string query = "INSERT INTO Users (pk_id, name) VALUES (@id, @username)";

    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        SqlCommand command = new SqlCommand(query, connection);

        // Bind the SQL parameters.
        SqlParameter idParam = new SqlParameter("@id", SqlDbType.Int, 0);
        SqlParameter nameParam = new SqlParameter("@username", SqlDbType.Text, 100);
        idParam.Value = 8;
        nameParam.Value = "First Region";
        command.Parameters.Add(idParam);
        command.Parameters.Add(nameParam);

        // Call Prepare 
        command.Prepare();
        command.ExecuteNonQuery();

        // Change parameter values and call ExecuteNonQuery again.
        command.Parameters[0].Value = 21;
        command.Parameters[1].Value = "Alex";
        command.ExecuteNonQuery();
    }

}
catch (Exception ex)
{
    //display error message
    Console.WriteLine("Exception: " + ex.Message);
}

Bei vorbereiteten Anweisungen müssen wir die Methode `Prepare()` nur einmal aufrufen. Danach können wir die Parameterwerte ändern und die Query mehrmals ausführen, ohne sie erneut zu kompilieren. 

Mit gespeicherten Prozeduren arbeiten 

Mit vorbereiteten Anweisungen können wir Query-Anweisungen in unserer Anwendung wiederverwenden. Anders als bei gespeicherten Prozeduren können wir eine vorbereitete Anweisung jedoch nicht von mehreren Anwendungen aus aufrufen oder Zugriffskontrollen implementieren.

Gespeicherte Prozeduren sind vorkompilierte Queries, die auf dem Datenbankserver gespeichert sind und wiederholt von mehreren Anwendungen aufgerufen werden können. Bei korrekter Implementierung schützen gespeicherte Prozeduren ebenso vor SQL-Injection-Angriffen wie vorbereitete Anweisungen. Wir können festlegen, wer eine gespeicherte Prozedur ausführen darf, und sogar den direkten Zugriff auf die zugrunde liegende Tabelle einschränken. So verringern wir die Auswirkungen einer möglichen SQL-Injection.

Wie vorbereitete Anweisungen unterstützen auch gespeicherte Prozeduren parametrisierte Queries und behandeln Nutzereingaben als Daten statt als SQL-Code. Außerdem bereinigt die Datenbank die an die Prozedur übergebenen Parameter automatisch. Dadurch wird verhindert, dass Angreifer schädlichen Code an die Datenbank senden.

Wir müssen gespeicherte Prozeduren zusammen mit Query-Parametern verwenden, anstatt dynamische Werte direkt in die Query einzufügen. Durch die klare Trennung zwischen Nutzereingaben und SQL-Anweisung verhindern wir, dass die SQL-Engine die Eingaben als Code interpretiert. 

Erstellen wir zur Veranschaulichung in unserer Datenbank eine gespeicherte Prozedur, die einen Wert für `Id` und `Name` entgegennimmt:

USE db;  
GO  
CREATE PROCEDURE InsertUser
    @Id INT,
    @Name VARCHAR(50)  
AS   
    INSERT INTO Users (pk_id, Name)
    VALUES (@Id, @Name);
GO

Wenn Sie SQL Server Management Studio verwenden, führen Sie die Prozedur im Query-Editor aus. Denken Sie daran, db durch den Namen Ihrer Datenbank zu ersetzen.

Hier sehen Sie ein Beispiel dafür, wie Sie mithilfe der gespeicherten Prozedur neue Nutzer in die Tabelle einfügen.

Verwenden Sie zunächst `Console.Readline()`, um über die Befehlszeile zur Eingabe eines Nutzernamens aufzufordern. Stellen Sie dann mit dem Befehl `SQLConnection` eine Verbindung zur Datenbank her. Rufen Sie anschließend die zuvor erstellte gespeicherte Prozedur `InsertUser` auf. 

Um die Parameter an eine gespeicherte Prozedur zu binden, verwenden Sie die Methode `Parameters.AddWithValue`. In diesem Beispiel geben wir für den Parameter `@id` einen fest codierten Wert an und übergeben den Nutzernamen aus der Nutzereingabe für den Parameter `@name`.

try
{
    string? username = Console.ReadLine();
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();

        // Call the stored procedure
        SqlCommand command = new SqlCommand("InsertUser", connection);
        command.CommandType = CommandType.StoredProcedure;

        // Bind the SQL parameters.
        command.Parameters.AddWithValue("@id", SqlDbType.Int).Value = 134;
        command.Parameters.AddWithValue("@name", SqlDbType.VarChar).Value = username;

        // Execute the query
        command.ExecuteNonQuery();
    }
}
catch (Exception ex)
{
    // Display error message
    Console.WriteLine("Exception: " + ex.Message);
}

Nachdem wir die gespeicherte Prozedur aufgerufen und die Variablen übergeben haben, führen wir die Query mit der Methode `ExecuteNonQuery` aus.

Zusätzliche Sicherheitsmaßnahmen für Datenbank-Queries 

Neben der Parametrisierung mit vorbereiteten Anweisungen und gespeicherten Prozeduren gibt es weitere Sicherheitsmaßnahmen, die SQL-Queries schützen.

Eingaben bereinigen und validieren

Wie bereits erwähnt, können Angreifer Eingabedaten ausnutzen, um schädlichen Code in der Datenbank auszuführen. Deshalb sind die Bereinigung und Validierung von Eingaben besonders wichtig. 

Bei der Bereinigung werden unerwünschte Zeichen (zum Beispiel `{}`, `'` und `/`) oder unsicheres HTML aus von Nutzern bereitgestellten Daten entfernt. Die Validierung stellt sicher, dass die Daten dem von der Datenbank erwarteten Format entsprechen. Sie können beispielsweise die Länge der Eingabe überprüfen und zu lange Eingaben zurückweisen. Auch Formate für E-Mail-Adressen und Datumsangaben lassen sich vorgeben. So verhindern Sie, dass Angreifer speziell formatierte Eingaben mit SQL-Anweisungen übermitteln.

Das Bereinigen und Validieren von Eingaben hilft Ihnen, die in SQL-Queries verwendeten Eingaben zu kontrollieren. Es ist jedoch keine narrensichere Lösung: Angreifer können Techniken wie Double-Encoding einsetzen, um Schutzmaßnahmen zu umgehen. So könnte ein Angreifer ein einfaches Anführungszeichen (`'`) als `%2527` statt als übliches `%27` codieren, um Eingabefilter zu umgehen, die nur einmal decodieren. Verwenden Sie die Eingabebereinigung daher nicht als Schutz vor SQL-Injection. Parametrisieren Sie stattdessen Ihre Queries und setzen Sie die Eingabebereinigung als zusätzliche Sicherheitsmaßnahme ein.

Ausgaben codieren

Wenn Sie Nutzereingaben in einer Webanwendung ausgeben, codieren Sie sie immer zuerst. Die Ausgabecodierung verhindert, dass Nutzerdaten als HTML, JavaScript oder SQL interpretiert werden.

Allowlists verwenden

Eine Query-Parametrisierung ist nicht immer möglich. Bestimmte Teile von SQL-Queries wie Tabellen- oder Spaltennamen werden nicht unterstützt. In solchen Fällen ist es besser, Eingaben anhand einer Allowlist zu validieren. Mit einer Allowlist können Sie strenge Regeln festlegen, die nur bestimmte Zeichen oder Muster in Eingaben zulassen. Dadurch werden schädliche Eingaben zurückgewiesen.

Eine Allowlist ist eine bessere Methode zur Verhinderung von SQL-Injection-Angriffen als eine Denylist. Eine Allowlist legt ausdrücklich fest, welche Eingaben zulässig sind. Eine Denylist hingegen lässt Spielraum – Angreifer können leicht neue Möglichkeiten finden, sie durch unterschiedliche Eingaben zu umgehen. Mit einer Allowlist behalten Sie letztlich mehr Kontrolle darüber, was akzeptiert wird.

Das Prinzip der geringsten Berechtigungen umsetzen

Um die Auswirkungen eines erfolgreichen SQL-Injection-Angriffs zu minimieren, sollten Sie stets das Prinzip der geringsten Berechtigungen (PoLP) anwenden. Das PoLP beschränkt die Zugriffsrechte von Nutzern auf das für ihre Arbeit erforderliche Maß.

Wenn ein Nutzerkonto beispielsweise nur Lesezugriff auf eine Tabelle benötigt, würden wir gemäß dem PoLP ausschließlich dieser Tabelle Leseberechtigungen erteilen. Benötigt der Nutzer nur Zugriff auf einen Teil der Tabellendaten, können wir eine Ansicht erstellen, die nur diese Daten enthält, und dem Konto Zugriff auf diese Ansicht statt auf die gesamte Tabelle gewähren. 

Wenn Sie gespeicherte Prozeduren verwenden, beschränken Sie die Ausführungsberechtigungen Ihrer Anwendung auf die benötigten Prozeduren. So schränken Sie den Zugriff eines Angreifers ein und minimieren die möglichen Auswirkungen – selbst wenn es zu einer SQL-Injection kommt.

Ihren Code mit Snyk schützen

Tests auf SQL-Injection können zusätzlichen Schutz vor SQL-Injection-Schwachstellen bieten. Automatische Tools für statische Anwendungssicherheitstests (SAST) wie Snyk Code helfen Ihnen, Sicherheitsprobleme frühzeitig zu erkennen.

Snyk bietet einen Schwachstellen-Scanner, der Ihren Code direkt in der IDE analysiert. Außerdem gibt es eine Visual Studio Code-Erweiterung, die Sie im Erweiterungsmanager finden. Folgen Sie den Schritten, um sie zu installieren und zu konfigurieren.

Sobald die Konfiguration abgeschlossen ist, können Sie das Tool verwenden, indem Sie in der Seitenleiste auf das Snyk-Logo klicken. Nun sollte eine Menüleiste wie im Bild unten angezeigt werden. Jeder Menüpunkt verfügt über einen Dropdown-Pfeil, auf den Sie zum Starten klicken können. Wir verwenden CODE SECURITY.

Dunkles Snyk-Navigationsmenü mit den Einträgen Open-Source-Sicherheit, Code-Sicherheit, Konfiguration, Codequalität und Hilfe & Feedback

Wenn wir eine Code-Sicherheitsanalyse für eine .NET-Anwendung ausführen, die keine parametrisierten Queries verwendet, weist Snyk uns auf eine SQL-Injection-Schwachstelle in unserem Projekt hin.

Sehen Sie sich die folgende Methode an, die mithilfe von Zeichenfolgenformatierung die Nutzereingabe an die Query anhängt:

public void withoutParameters(string dataSource, string initialCat, string persistSecurity, string userId, string password)
    {
        string connectionString = $"Data Source={dataSource};Initial Catalog={initialCat};Persist Security Info={persistSecurity};User ID={userId};Password={password}";
        try
        {

            string username = Console.ReadLine();

            using (SqlConnection connection = new SqlConnection(connectionString))
            {
            connection.Open();
            string query = $"INSERT INTO Users(pk_id, name) VALUES (48,'{username}');";
                using (SqlCommand command = new SqlCommand(query, connection))
                {
                    command.Parameters.AddWithValue("@username", username);

                    using (SqlDataReader reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            Console.WriteLine("{0} {1}", reader.GetString(0), reader.GetString(1));
                        }
                    }
                }
            }
        }
        catch (Exception ex)
            {
                //display error message
                Console.WriteLine("Exception: " + ex.Message);
            }
 }

Führen wir für den obigen Code eine Code-Sicherheitsanalyse aus, findet Snyk eine Schwachstelle, wie im folgenden Screenshot dargestellt.

Snyk Code-Sicherheitslücken-Panel: Nicht bereinigte Befehlszeileneingabe fließt in einen SQL-Befehl ein. Auf Zeile 28 wird eine SQL-Injection-Schwachstelle angezeigt.

Der Analysebericht weist auf eine nicht bereinigte Eingabe aus dem Befehlszeilenargument hin, die in der SQL-Query verwendet wird und möglicherweise zu einer SQL-Injection-Schwachstelle führt. Snyk nennt die Zeile und Datei, die die Schwachstelle enthalten. Außerdem werden Beispiele für mögliche Korrekturen angezeigt, damit wir Schwachstellen schneller beheben können.

Fazit

Die Query-Parametrisierung ist entscheidend, um SQL-Injection-Angriffe zu verhindern. Sie stellt sicher, dass Nutzereingaben als Daten und nicht als ausführbarer Code behandelt werden, und mindert so wirksam das Risiko schädlicher SQL-Injection. 

Wir haben die Vorteile der Parametrisierung von Abfragen mit oder ohne vorbereitete Anweisungen erläutert. Dabei haben wir festgestellt, dass die erste Option mehr Sicherheitsvorteile bietet, da sie SQL-Logik und Benutzereingaben voneinander trennt und so manuelles Escaping oder Bereinigen überflüssig macht. Vorbereitete Anweisungen verbessern außerdem die Performance, da Abfragen vorkompiliert werden.

Eine Alternative zu vorbereiteten Anweisungen sind gespeicherte Prozeduren. In Verbindung mit Abfrageparametern bieten sie eine zusätzliche Sicherheitsebene, da Sie kontrollieren können, wer Zugriff hat.

Um die Sicherheit Ihrer Anwendung weiter zu erhöhen, können wir den Schwachstellenscanner von Snyk in den Entwicklungsworkflow integrieren. Durch regelmäßige Scans unserer Codebasis können wir Schwachstellen identifizieren, die zu SQL-Injection führen können. So können wir potenzielle Sicherheitsrisiken bereits während der Entwicklung beheben und die Wahrscheinlichkeit verringern, dass Angreifer auf unsere Anwendung zugreifen.

Starten Sie mit Capture the Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.

Gepostet in:

Weiterlesen

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSec: Sicherheit, die mit der KI-Entwicklung Schritt hält

KI-gestützte Entwicklung überholt herkömmliche Security-Workflows. Snyk Agentic AppSec setzt Security-Agenten ein, die Schwachstellen kontinuierlich finden, beheben und überprüfen – während Menschen die Maßstäbe festlegen.

feature insights context
Blog

Autonome Angriffe sind bereits Realität. Die Verteidigung muss Schritt halten.

Autonome Angreifer verkürzen das Zeitfenster für die Verteidigung. Erfahren Sie, wie kontinuierliches Erkennen, Beheben, Validieren und Verhindern Sicherheitsteams helfen kann, Schritt zu halten.

illustration hero ai
Blog

Der KI-Hurrikan ist da

KI beschleunigt gleichermaßen die Softwareentwicklung und Cyberangriffe. Führungskräfte müssen Agents und Code von Anfang an absichern, zur Laufzeit Kontrollen durchsetzen und Abwehrmaßnahmen unabhängig validieren.