SQL-Injection

OWASP-Kategorie: MASVS-CODE: Codequalität

Übersicht

Bei einer SQL-Injection werden Sicherheitslücken in Anwendungen ausgenutzt, indem Code in SQL-Anweisungen eingefügt wird, um über die beabsichtigten Schnittstellen hinaus auf zugrunde liegende Datenbanken zuzugreifen. Der Angriff kann private Daten offenlegen, Datenbankinhalte beschädigen und sogar die Back-End-Infrastruktur gefährden.

SQL kann anfällig für Einschleusungen über Abfragen sein, die dynamisch erstellt werden, indem Nutzereingaben vor der Ausführung verkettet werden. SQL-Injection zielt auf Web-, Mobil- und alle SQL-Datenbankanwendungen ab und gehört in der Regel zu den OWASP Top Ten der Web-Sicherheitslücken. Angreifer haben die Technik bei mehreren hochkarätigen Sicherheitsverletzungen eingesetzt.

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

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

Ein solcher Code würde in einer Webkonsole einen Datenbank-Syntaxfehler erzeugen, der zeigt, dass die Anwendung anfällig für SQL-Injection sein kann. Wenn Sie die Bestellnummer durch 'OR 1=1– ersetzen, kann die Authentifizierung erreicht werden, da die Datenbank die Anweisung als True auswertet, da eins immer gleich eins ist.

Ebenso gibt diese Abfrage 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 für die Freigabe für andere Apps exportiert werden kann. Berechtigungen sollten nach dem Prinzip der geringsten Berechtigung festgelegt werden. Ein exportierter ContentProvider kann eine einzige angegebene Berechtigung zum Lesen und Schreiben haben.

Es ist wichtig zu beachten, dass nicht alle SQL-Injections zu einer Ausnutzung führen. Einige Contentanbieter gewähren Lesern bereits vollständigen Zugriff auf die SQLite-Datenbank. Die Möglichkeit, beliebige Abfragen auszuführen, bietet nur wenig Vorteile. Muster, die ein Sicherheitsproblem darstellen können, sind:

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

Auswirkungen

SQL-Injection kann sensible Nutzer- oder Anwendungsdaten offenlegen, Authentifizierungs- und Autorisierungseinschränkungen überwinden und Datenbanken anfällig für Beschädigung oder Löschung 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, geistiges Eigentum oder das Vertrauen der Nutzer zu verlieren.

Gegenmaßnahmen

Austauschbare 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 ist ein 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-Schnittstelle vorkompiliert SQL-Anweisungen als Objekt, das dann mehrmals effizient ausgeführt werden kann. `PreparedStatement` verwendet ? als Platzhalter für Parameter, wodurch der folgende kompilierte Einschleusungsversuch unwirksam wird:

WHERE id=295094 OR 1=1;

In diesem Fall wird die Anweisung 295094 OR 1=1 als Wert für die ID gelesen, was wahrscheinlich keine Ergebnisse liefert. Bei einer Rohabfrage würde die Anweisung OR 1=1 als weiterer 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 die selection und selectionArgs der Methode query() kombiniert, um eine WHERE Klausel zu erstellen. Da die Argumente separat angegeben werden, werden sie vor der Kombination maskiert, wodurch SQL-Injection verhindert wird.

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

Ordnungsgemäß konfigurierte SQLiteQueryBuilder verwenden

Entwickler können Anwendungen weiter schützen, indem sie SQLiteQueryBuilder verwenden, eine Klasse, mit der Abfragen erstellt werden, die an SQLiteDatabase Objekte gesendet werden sollen. Empfohlene Konfigurationen sind:

Room-Bibliothek verwenden

Das Paket android.database.sqlite bietet APIs, die für die Verwendung von Datenbanken unter Android erforderlich sind. Dieser Ansatz erfordert jedoch das Schreiben von Code auf niedriger Ebene und bietet keine Kompilierzeitprüfung von Roh-SQL-Abfragen. Wenn sich Datendiagramme ändern, müssen die betroffenen SQL-Abfragen manuell aktualisiert werden. Das ist ein zeitaufwendiger und fehleranfälliger Prozess.

Eine Lösung auf hoher Ebene ist die Verwendung der Room-Persistenzbibliothek als Abstraktionsebene für SQLite-Datenbanken. Die Funktionen von Room umfassen:

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

Die Vorteile von Room sind:

  • Kompilierzeitprüfung von SQL-Abfragen.
  • Reduzierung von fehleranfälligem Boilerplate-Code.
  • Vereinfachte Datenbankmigration.

Best Practices

SQL-Injection ist ein starker Angriff, gegen den es schwierig sein kann, vollständig widerstandsfähig zu sein, insbesondere bei großen und komplexen Anwendungen. Es sollten zusätzliche Sicherheitsmaßnahmen vorhanden sein, um die Schwere potenzieller Fehler in Datenschnittstellen zu begrenzen, darunter:

  • Robuste, unidirektionale und gesalzene Hashes zum Verschlüsseln von Passwörtern:
    • 256-Bit-AES für kommerzielle Anwendungen.
    • 224- oder 256-Bit-Schlüsselgrößen für die Elliptische-Kurven-Kryptografie.
  • Berechtigungen einschränken.
  • Datenformate genau strukturieren und prüfen, ob die Daten dem erwarteten Format entsprechen.
  • Wenn möglich, keine personenbezogenen oder sensiblen Nutzerdaten speichern (z. B. Anwendungslogik durch Hashing implementieren, anstatt Daten zu übertragen oder zu speichern).
  • Anzahl der APIs und Drittanbieteranwendungen, die auf sensible Daten zugreifen, minimieren.

Ressourcen