## SQL-Injection

SQL-Injection ist eine Technik, bei der ein Angreifer Schwachstellen im Anwendungscode ausnutzt, mit dem dynamische SQL-Abfragen erstellt werden. Ein Angreifer kann sich Zugang zu besonders geschützten Bereichen der Anwendung verschaffen, sämtliche Informationen aus der Datenbank abrufen, vorhandene Daten manipulieren oder sogar gefährliche Befehle auf der Systemebene des Datenbank-Hosts ausführen. Die Schwachstelle entsteht, wenn Entwickler in ihren SQL-Anweisungen beliebige Eingaben miteinander verknüpfen oder einfügen.

Beispiel #1 Die Ergebnisliste in mehrere Seiten aufsplitten ... und Superuser anlegen (PostgreSQL)

Im folgenden Beispiel wird die Benutzereingabe direkt in die SQL-Abfrage eingefügt, wodurch der Angreifer ein Superuser-Konto für die Datenbank erlangen kann.

    <?php

    $offset = $_GET['offset']; // Vorsicht, keine Validierung der Eingabe!
    $query  = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
    $result = pg_query($conn, $query);

    ?>

Normale Benutzer klicken auf die 'nächste'- bzw. 'vorige'-Links, wo der $offset in der URL enthalten ist. Das Skript erwartet, dass der ankommende $offset eine Zahl enthält. Was aber, wenn jemand versucht einzubrechen, indem er das Folgende an die URL anhängt:

    0;
    insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
        select 'crack', usesysid, 't','t','crack'
        from pg_shadow where usename='postgres';
    --

In diesem Fall würde das Skript dem Angreifer einen Superuser-Zugang zur Verfügung stellen. Beachten Sie, dass `0;` einen gültigen Offset zur ursprünglichen Abfrage liefert, und sie beendet.

> Hinweis:

> Es ist eine übliche Technik, den SQL-Parser mit dem SQL-Kommentarzeichen `--` zu zwingen, den Rest der vom Entwickler geschriebenen Abfrage zu ignorieren.

Eine gangbare Methode, an Passwörter zu gelangen, ist, Ihre Seiten mit den Suchergebnissen zu umgehen. Der Angreifer braucht nur zu probieren, ob irgendeine übertragene Variable, die in dem SQL-Statement verwendet wird, nicht richtig gehandhabt wird. Diese Filter können gewöhnlich in einem vorausgehenden Formular gesetzt werden, indem `WHERE-, ORDER BY-, LIMIT-` und `OFFSET`-Klauseln in `SELECT`-Statements angepasst werden. Wenn Ihre Datenbank das `UNION`-Konstrukt unterstützt, kann der Angreifer versuchen, eine komplette Abfrage an das Original anzuhängen, um Passwörter aus einer beliebigen Tabelle aufzulisten. Es wird dringend empfohlen, nur sichere Hashes von Passwörtern zu speichern und nicht die Passwörter selbst.

Beispiel #2 Artikel auflisten ... und ein paar Passwörter (beliebiger Datenbankserver)

    <?php

    $query  = "SELECT id, name, inserted, size FROM products
               WHERE size = '$size'";
    $result = odbc_exec($conn, $query);

    ?>

Der statische Teil der Abfrage kann mit einem anderen `SELECT`-Statement kombiniert werden, welches alle Passwörter preisgibt

    '
    union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable;
    --

Auch `UPDATE`- und `INSERT`-Anweisungen sind für derartige Angriffe anfällig.

Beispiel #3 Vom Zurücksetzen eines Passworts ... zum Erlangen von mehr Rechten (beliebiger Datenbankserver)

    <?php
    $query = "UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
    ?>

Wenn ein böswilliger Benutzer den Wert `' or uid like'%admin%` an $uid übermittelt, um das Administrator-Passwort zu ändern, oder einfach $pwd auf `hehehe', trusted=100, admin='yes` setzt, um mehr Rechte zu erhalten, dann wird die Abfrage verdreht:

    <?php

    // $uid: ' or uid like '%admin%
    $query = "UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%';";

    // $pwd: hehehe', trusted=100, admin='yes
    $query = "UPDATE usertable SET pwd='hehehe', trusted=100, admin='yes' WHERE
    ...;";

    ?>

Obwohl es offensichtlich ist, dass ein Angreifer zumindest ein wenig Kenntnis der genutzten Datenbankarchitektur besitzen muss, um einen erfolgreichen Angriff durchzuführen, ist es oft sehr einfach, diese Informationen zu erhalten. Der Code kann z. B. Teil einer Open-Source-Software sein und öffentlich zugänglich sein. Diese Informationen können auch durch Closed-Source-Code preisgegeben werden - selbst wenn dieser kodiert, verschleiert oder kompiliert ist - und sogar durch Ihren eigenen Code durch die Anzeige von Fehlermeldungen. Andere Methoden beinhalten die Nutzung typischer Tabellen- und Spaltennamen. Ein Login-Formular etwa, dass eine Tabelle 'users' mit den Spaltennamen 'id', 'username' und 'password' nutzt.

Beispiel #4 Angriff auf das Betriebssystem des Datenbank-Hosts (MSSQL-Server)

Ein erschreckendes Beispiel, wie der Zugriff auf Befehle auf Betriebssystemebene bei manchen Datenbankservern erfolgen kann:

    <?php

    $query  = "SELECT * FROM products WHERE id LIKE '%$prod%'";
    $result = mssql_query($query);

    ?>

Wenn ein Angreifer den Wert `a%' exec master..xp_cmdshell 'net user test testpass /ADD' --` auf $prod überträgt, wird $query zu:

    <?php

    $query  = "SELECT * FROM products
               WHERE id LIKE '%a%'
               exec master..xp_cmdshell 'net user test testpass /ADD' --%'";
    $result = mssql_query($query);

    ?>

Der MSSQL-Server führt die SQL-Statements im Batch aus, inklusive eines Kommandos um einen neuen Benutzer zur Datenbank der lokalen Konten hinzuzufügen. Würde diese Anwendung als `sa` und der MSSQLSERVER-Service mit genügend Rechten laufen, hätte der Angreifer nun ein Konto, mit welchem er Zugriff auf diese Maschine hätte.

> Hinweis:

> Manche der obigen Beispiele sind an einen spezifischen Datenbankserver gebunden, aber das bedeutet nicht, dass ein ähnlicher Angriff auf andere Produkte unmöglich wäre. Ihr Datenbankserver könnte auf andere Weise genauso verwundbar sein.

Bild mit freundlicher Genehmigung von » xkcd

### Techniken zur Vermeidung

Um das Risiko einer SQL-Injection zu verringern, wird empfohlen, alle Daten über vorbereitete Anweisungen zu binden. Die Verwendung von parametrisierten Abfragen ist zwar nicht ausreichend, um SQL-Injections vollständig zu verhindern, aber es ist der einfachste und sicherste Weg, um Daten für SQL-Anweisungen bereitzustellen. Alle dynamischen Datenliterale in `WHERE`-, `SET`- und `VALUES`-Klauseln müssen durch Platzhalter ersetzt werden. Die eigentlichen Daten werden bei der Ausführung gebunden und getrennt vom SQL-Befehl gesendet.

Die Parameterbindung kann nur für Daten verwendet werden. Andere dynamische Teile der SQL-Abfrage müssen gegen eine bekannte Liste zulässiger Werte gefiltert werden.

Beispiel #5 Ein sicherer Weg, eine Abfrage zu erstellen

    <?php

    // Der dynamische SQL-Teil wird anhand der erwarteten Werte validiert
    $sortingOrder = $_GET['sortingOrder'] === 'DESC' ? 'DESC' : 'ASC';
    $productId = $_GET['productId'];
    // Das SQL wird mit einem Platzhalter vorbereitet
    $stmt = $pdo->prepare("SELECT * FROM products WHERE id LIKE ? ORDER BY price {$sortingOrder}");
    // Der Wert wird mit LIKE-Wildcards versehen
    $stmt->execute(["%{$productId}%"]);

    ?>

Vorbereitete Anweisungen werden von PDO, MySQLi und anderen Bibliotheken angeboten.

Angriffe durch SQL-Injection basieren hauptsächlich darauf, Code auszunutzen, der nicht unter dem Aspekt der Sicherheit geschrieben wurde. Vertrauen Sie niemals einer Eingabe, insbesondere nicht von der Clientseite, selbst wenn sie von einer Auswahlbox, einem versteckten Eingabefeld oder einem Cookie kommt. Das erste Beispiel zeigt, dass eine so einfache Abfrage zu einer Katastrophe führen kann.

Eine umfassende Verteidigungsstrategie umfasst mehrere bewährte Programmierpraktiken:

- Stellen Sie nie als Superuser oder Eigentümer einer Datenbank eine Verbindung zur Datenbank her. Verwenden Sie immer speziell angelegte Benutzer mit sehr limitierten Rechten.
- Prüfen Sie, ob die gegebene Eingabe dem erwarteten Datentyp entspricht. PHP bietet eine große Auswahl an Funktionen zum Validieren der Eingaben, von den einfachsten unter Variablenfunktionen und Character-Type-Funktionen (z. B. is_numeric(), bzw. ctype_digit()), bis hin zu den Perl-kompatiblen regulären Ausdrücken.
- Wenn die Anwendung eine numerische Eingabe erwartet, sollten Sie die Daten mittels ctype_digit() überprüfen, den Typ stillschweigend mittels settype() ändern oder deren numerische Darstellung mittels sprintf() verwenden.
- Unterstützt die Datenbank-Ebene das Binden von Variablen nicht, so maskieren Sie jede nichtnumerische Benutzereingabe, welche zur Datenbank weitergereicht werden soll, mit der jeweiligen datenbankspezifischen Maskierungsfunktion für Zeichenketten (z. B. mysql_real_escape_string(), sqlite_escape_string() usw.). Generische Funktionen wie addslashes() sind nur in einer sehr spezifischen Umgebung nützlich (z. B. MySQL mit einem SingleByte-Zeichensatz bei deaktiviertem NO_BACKSLASH_ESCAPES), sodass es besser ist, diese zu vermeiden.
- Geben Sie um keinen Preis irgendwelche datenbankspezifischen Informationen aus, speziell über das Schema. Siehe auch Fehlerbehandlung und Fehlerbehandlungsfunktionen.

Abgesehen davon profitieren Sie von einer Protokollierung der Abfragen entweder in Ihrem Skript oder durch die Datenbank selbst, falls sie Protokollierung unterstützt. Klar, die Protokollierung kann keinen schädlichen Versuch verhindern, aber sie kann helfen herauszufinden, welche Anwendung umgangen wurde. Das Protokoll ist nicht von sich aus nützlich, aber durch die in ihm enthaltenen Informationen, und je mehr Details es enthält, desto besser ist es im Allgemeinen.

`↑` and `↓` to navigate • `Enter` to select • `Esc` to close • `/` to open