SQL-Injection

OWASP-Kategorie: MASVS-CODE: Codequalität

Übersicht

Bei einer SQL-Injection werden anfällige Anwendungen ausgenutzt, indem Code in SQL-Anweisungen eingefügt wird, um über die absichtlich freigegebenen Schnittstellen hinaus auf die zugrunde liegenden Datenbanken zuzugreifen. Der Angriff kann private Daten offenlegen, Datenbankinhalte beschädigen und sogar die Back-End-Infrastruktur schädigen.

SQL kann anfällig für Injections über Abfragen sein, die dynamisch erstellt werden, indem Nutzereingaben vor der Ausführung verknüpft werden. SQL-Injection ist eine der Top 10-Weblücken von OWASP und richtet sich gegen Webanwendungen, mobile Anwendungen und SQL-Datenbankanwendungen. Angreifer haben diese Technik bei mehreren medienwirksamen Sicherheitsverletzungen eingesetzt.

In diesem einfachen Beispiel kann eine nicht entescapede Eingabe eines Nutzers in ein Feld für die Bestellnummer in den SQL-String eingefügt und als folgende Abfrage interpretiert werden:

SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1

Dieser Code würde in einer Webansicht einen Datenbanksyntaxfehler generieren, der darauf hinweist, dass die Anwendung möglicherweise anfällig für SQL-Injection ist. Wenn Sie die Bestellnummer durch 'OR 1=1– ersetzen, kann die Authentifizierung erfolgen, da die Datenbank die Anweisung auf True auswertet, da Eins immer Eins ist.

Diese Abfrage gibt ebenfalls alle Zeilen aus einer Tabelle zurück:

SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;

Contentanbieter

Contentanbieter bieten einen strukturierten Speichermechanismus, der auf eine Anwendung beschränkt oder zur Freigabe für andere Apps exportiert werden kann. Berechtigungen sollten gemäß dem Prinzip der geringsten Berechtigung festgelegt werden. Eine exportierte ContentProvider kann eine einzelne Berechtigung zum Lesen und Schreiben haben.

Beachten Sie, dass nicht alle SQL-Injections zu einer Ausnutzung führen. Einige Inhaltsanbieter gewähren Lesern bereits vollständigen Zugriff auf die SQLite-Datenbank. Die Möglichkeit, beliebige Abfragen auszuführen, bietet keinen großen Vorteil. Beispiele für Muster, die ein Sicherheitsproblem darstellen können:

  • Mehrere Inhaltsanbieter, die eine einzige SQLite-Datenbankdatei gemeinsam nutzen.
    • In diesem Fall kann jede Tabelle für einen einzelnen Inhaltsanbieter bestimmt sein. Eine erfolgreiche SQL-Injection bei einem Contentanbieter würde Zugriff auf alle anderen Tabellen gewähren.
  • Ein Contentanbieter hat mehrere Berechtigungen für Inhalte in derselben Datenbank.
    • Eine SQL-Injection bei einem einzelnen Contentanbieter, der Zugriff mit unterschiedlichen Berechtigungsebenen gewährt, kann zu einer lokalen Umgehung von Sicherheits- oder Datenschutzeinstellungen führen.

Positiv beeinflussen

SQL-Injections können sensible Nutzer- oder Anwendungsdaten offenlegen, Authentifizierungs- und Autorisierungsbeschränkungen überwinden und Datenbanken anfällig für Beschädigungen oder Löschungen machen. Die Auswirkungen können gefährliche und dauerhafte Folgen für Nutzer haben, deren personenbezogene Daten offengelegt wurden. Anbieter von Apps und Diensten riskieren, ihr geistiges Eigentum oder das Vertrauen der Nutzer zu verlieren.

Abhilfemaßnahmen

Ersetzbare Parameter

Wenn Sie ? als austauschbaren Parameter in Auswahlklauseln und ein separates Array von Auswahlargumenten verwenden, wird die Nutzereingabe direkt an die Abfrage gebunden, anstatt sie als Teil einer SQL-Anweisung zu interpretieren.

Kotlin

// Constructs a selection clause with a replaceable parameter.
val selectionClause = "var = ?"

// Sets up an array of arguments.
val selectionArgs: Array<String> = arrayOf("")

// Adds values to the selection arguments array.
selectionArgs[0] = userInput

Java

// Constructs a selection clause with a replaceable parameter.
String selectionClause =  "var = ?";

// Sets up an array of arguments.
String[] selectionArgs = {""};

// Adds values to the selection arguments array.
selectionArgs[0] = userInput;

Die Nutzereingabe wird direkt an die Abfrage gebunden und nicht als SQL behandelt, wodurch Code-Injection verhindert wird.

Hier ein etwas ausführlicheres Beispiel für die Abfrage einer Shopping-App zum Abrufen von Kaufdetails mit austauschbaren Parametern:

Kotlin

fun validateOrderDetails(email: String, orderNumber: String): Boolean {
    val cursor = db.rawQuery(
        "select * from purchases where EMAIL = ? and ORDER_NUMBER = ?",
        arrayOf(email, orderNumber)
    )

    val bool = cursor?.moveToFirst() ?: false
    cursor?.close()

    return bool
}

Java

public boolean validateOrderDetails(String email, String orderNumber) {
    boolean bool = false;
    Cursor cursor = db.rawQuery(
      "select * from purchases where EMAIL = ? and ORDER_NUMBER = ?", 
      new String[]{email, orderNumber});
    if (cursor != null) {
        if (cursor.moveToFirst()) {
            bool = true;
        }
        cursor.close();
    }
    return bool;
}

PreparedStatement-Objekte verwenden

Die PreparedStatement-Benutzeroberfläche kompiliert SQL-Anweisungen vorab als Objekt, das dann effizient mehrmals ausgeführt werden kann. PreparedStatement verwendet ? als Platzhalter für Parameter, was den folgenden kompilierten Injection-Versuch unwirksam machen würde:

WHERE id=295094 OR 1=1;

In diesem Fall wird die 295094 OR 1=1-Anweisung als Wert für „ID“ gelesen, was wahrscheinlich zu keinen Ergebnissen führt. Bei einer Rohabfrage wird die OR 1=1-Anweisung dagegen als Teil der WHERE-Klausel interpretiert. Das folgende Beispiel zeigt eine parametrisierte Abfrage:

Kotlin

val pstmt: PreparedStatement = con.prepareStatement(
        "UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?").apply {
    setString(1, "Barista")
    setInt(2, 295094)
}

Java

PreparedStatement pstmt = con.prepareStatement(
                                "UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?");
pstmt.setString(1, "Barista")   
pstmt.setInt(2, 295094)

Abfragemethoden verwenden

In diesem längeren Beispiel werden selection und selectionArgs der Methode query() zu einer WHERE-Klausel kombiniert. Da die Argumente separat angegeben werden, werden sie vor der Kombination geescaped, um SQL-Injection zu verhindern.

Kotlin

val db: SQLiteDatabase = dbHelper.getReadableDatabase()
// Defines a projection that specifies which columns from the database
// should be selected.
val projection = arrayOf(
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
)

// Filters results WHERE "title" = 'My Title'.
val selection: String = FeedEntry.COLUMN_NAME_TITLE.toString() + " = ?"
val selectionArgs = arrayOf("My Title")

// Specifies how to sort the results in the returned Cursor object.
val sortOrder: String = FeedEntry.COLUMN_NAME_SUBTITLE.toString() + " DESC"

val cursor = db.query(
    FeedEntry.TABLE_NAME,  // The table to query
    projection,            // The array of columns to return
                           //   (pass null to get all)
    selection,             // The columns for the WHERE clause
    selectionArgs,         // The values for the WHERE clause
    null,                  // Don't group the rows
    null,                  // Don't filter by row groups
    sortOrder              // The sort order
).use {
    // Perform operations on the query result here.
    it.moveToFirst()
}

Java

SQLiteDatabase db = dbHelper.getReadableDatabase();
// Defines a projection that specifies which columns from the database
// should be selected.
String[] projection = {
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
};

// Filters results WHERE "title" = 'My Title'.
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// Specifies how to sort the results in the returned Cursor object.
String sortOrder =
    FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,   // The table to query
    projection,             // The array of columns to return (pass null to get all)
    selection,              // The columns for the WHERE clause
    selectionArgs,          // The values for the WHERE clause
    null,                   // don't group the rows
    null,                   // don't filter by row groups
    sortOrder               // The sort order
    );

Richtig konfigurierten SQLiteQueryBuilder verwenden

Entwickler können Anwendungen mithilfe von SQLiteQueryBuilder weiter schützen. Diese Klasse hilft beim Erstellen von Abfragen, die an SQLiteDatabase-Objekte gesendet werden. Zu den empfohlenen Konfigurationen gehören:

Room-Bibliothek verwenden

Das Paket android.database.sqlite bietet APIs, die für die Verwendung von Datenbanken auf Android-Geräten erforderlich sind. Bei diesem Ansatz muss jedoch Low-Level-Code geschrieben werden und es fehlt die Überprüfung von Roh-SQL-Abfragen zur Kompilierungszeit. Wenn sich Datendiagramme ändern, müssen die betroffenen SQL-Abfragen manuell aktualisiert werden. Das ist zeitaufwendig und fehleranfällig.

Eine allgemeine Lösung besteht darin, die Room Persistence Library als Abstraktionsschicht für SQLite-Datenbanken zu verwenden. Die Zimmerausstattung umfasst:

  • Eine Datenbankklasse, die als Hauptzugangspunkt für die Verbindung zu den gespeicherten Daten der App dient.
  • Datenentitäten, die die Tabellen der Datenbank darstellen.
  • Datenzugriffsobjekte (DAOs), die Methoden bereitstellen, mit denen die App Daten abfragen, aktualisieren, einfügen und löschen kann.

Zu den Vorteilen des Raums gehören:

  • Überprüfung von SQL-Abfragen zur Kompilierungszeit
  • Reduzierung fehleranfälliger Standardcodes.
  • Optimierte Datenbankmigration

Best Practices

SQL-Injection ist ein potenter Angriff, gegen den es schwierig sein kann, vollständig resistent zu sein, insbesondere bei großen und komplexen Anwendungen. Es sollten zusätzliche Sicherheitsaspekte berücksichtigt werden, um die Schwere potenzieller Mängel in Datenschnittstellen zu begrenzen. Dazu gehören:

  • Robuste, unidirektionale und gesalzene Hashes zum Verschlüsseln von Passwörtern:
    • 256-Bit-AES für kommerzielle Anwendungen.
    • 224- oder 256-Bit-Öffentliche-Schlüssel-Größen für die elliptische-Kurven-Kryptografie.
  • Berechtigungen einschränken
  • Datenformate genau strukturieren und prüfen, ob die Daten dem erwarteten Format entsprechen.
  • Speichern Sie nach Möglichkeit keine personenbezogenen oder sensiblen Nutzerdaten. Implementieren Sie beispielsweise die Anwendungslogik durch Hash-Technologie, anstatt Daten zu übertragen oder zu speichern.
  • Minimieren von APIs und Drittanbieteranwendungen, die auf vertrauliche Daten zugreifen.

Ressourcen