Sie meinen also, Ihre SQL-Datenbank ist leistungsfähig und sicher vor plötzlicher Zerstörung? Nun, die SQL-Injection sieht das anders!
Ja, wir sprechen hier von plötzlicher Zerstörung, denn ich möchte diesen Artikel nicht mit dem üblichen, langweiligen Gerede von „Sicherheitsverbesserung“ und „Verhinderung böswilligen Zugriffs“ beginnen. SQL-Injection ist ein so alter Hut, dass jeder, wirklich jeder Entwickler, davon weiß und wie man sie vermeidet. Abgesehen von dem einen Mal, wenn sie einen Fehler machen, und die Folgen können verheerend sein.
Wenn Sie bereits mit der SQL-Injection vertraut sind, können Sie gerne zur zweiten Hälfte des Artikels springen. Für diejenigen jedoch, die gerade erst in der Welt der Webentwicklung Fuß fassen und davon träumen, aufzusteigen, ist eine Einführung angebracht.
Was genau ist SQL-Injection?
Der Schlüssel zum Verständnis der SQL-Injection liegt im Namen: SQL + Injection. Das Wort „Injection“ hat hier nichts mit medizinischen Konnotationen zu tun, sondern bezieht sich auf die Bedeutung des Verbs „injizieren“. Zusammengenommen vermitteln diese beiden Wörter die Idee, SQL-Code in eine Webanwendung „einzuschleusen“.
SQL-Code in eine Webanwendung einschleusen … hmmm … tun wir das nicht ohnehin? Ja, aber wir wollen nicht, dass ein Angreifer unsere Datenbank übernimmt. Betrachten wir das an einem Beispiel:
Nehmen wir an, Sie entwickeln eine typische PHP-Website für ein lokales E-Commerce-Geschäft und beschließen, ein Kontaktformular hinzuzufügen, das so aussieht:
<form action="record_message.php" method="POST"> <label>Ihr Name</label> <input type="text" name="name"> <label>Ihre Nachricht</label> <textarea name="message" rows="5"></textarea> <input type="submit" value="Senden"> </form>
Und nehmen wir an, die Datei send_message.php speichert alle Daten in einer Datenbank, damit die Ladenbesitzer später die Nachrichten der Benutzer lesen können. Der Code könnte so aussehen:
<?php $name = $_POST['name']; $message = $_POST['message']; // Prüfen, ob dieser Benutzer bereits eine Nachricht hat mysqli_query($conn, "SELECT * from messages where name = $name"); // weiterer Code
Sie versuchen also zunächst zu prüfen, ob dieser Benutzer bereits eine ungelesene Nachricht hat. Die Abfrage `SELECT * from messages where name = $name` scheint doch einfach genug, oder?
FALSCH!
In unserer Unschuld haben wir die Türen zur sofortigen Zerstörung unserer Datenbank geöffnet. Damit dies funktioniert, müssen für den Angreifer folgende Bedingungen erfüllt sein:
- Die Anwendung läuft mit einer SQL-Datenbank (was heutzutage fast jede Anwendung tut).
- Die aktuelle Datenbankverbindung verfügt über „Bearbeiten“- und „Löschen“-Rechte für die Datenbank.
- Die Namen wichtiger Tabellen können erraten werden.
Der dritte Punkt bedeutet, dass ein Angreifer, der weiß, dass Sie einen E-Commerce-Shop betreiben, davon ausgeht, dass die Bestelldaten sehr wahrscheinlich in einer Tabelle namens „Bestellungen“ gespeichert werden. Mit diesen Informationen muss der Angreifer nur noch Folgendes als seinen Namen eingeben:
Joe; truncate orders; – Ja, richtig! Schauen wir mal, was aus der Abfrage wird, wenn sie vom PHP-Skript ausgeführt wird:
SELECT * FROM Nachrichten WHERE Name = Joe; truncate orders;
Okay, der erste Teil der Abfrage hat einen Syntaxfehler (keine Anführungszeichen um „Joe“), aber das Semikolon zwingt die MySQL-Engine, mit der Interpretation einer neuen Anweisung zu beginnen: truncate orders. Und schon ist die gesamte Bestellhistorie weg!
Nachdem Sie nun wissen, wie SQL-Injection funktioniert, ist es an der Zeit, sich anzusehen, wie Sie sie verhindern können. Die beiden Bedingungen, die für eine erfolgreiche SQL-Injection erfüllt sein müssen, sind:
SQL-Injection in PHP verhindern
Da Datenbankverbindungen, Abfragen und Benutzereingaben nun mal Teil des Lebens sind, wie können wir SQL-Injection verhindern? Zum Glück ist es recht einfach, und es gibt zwei Möglichkeiten: 1) Bereinigen der Benutzereingaben und 2) Verwenden von vorbereiteten Anweisungen.
Benutzereingaben bereinigen
Wenn Sie eine ältere PHP-Version verwenden (5.5 oder niedriger, was auf Shared-Hosting-Servern häufig vorkommt), ist es ratsam, alle Benutzereingaben über eine Funktion namens `mysql_real_escape_string()` laufen zu lassen. Diese Funktion entfernt im Wesentlichen alle Sonderzeichen in einer Zeichenkette, sodass sie ihre besondere Bedeutung verlieren, wenn sie von der Datenbank verwendet werden.
Wenn Sie beispielsweise eine Zeichenkette wie `Ich bin ein ‚String’` haben, kann das einfache Anführungszeichen (‚) von einem Angreifer verwendet werden, um die erstellte Datenbankabfrage zu manipulieren und eine SQL-Injection zu verursachen. Wenn Sie die Zeichenkette jedoch durch `mysql_real_escape_string()` laufen lassen, wird daraus `Ich bin ein \’String’`, wodurch das einfache Anführungszeichen mit einem Backslash maskiert wird. Dadurch wird die gesamte Zeichenkette als harmlose Zeichenkette an die Datenbank übergeben und kann nicht mehr zur Manipulation der Abfrage verwendet werden.
Dieser Ansatz hat einen Nachteil: Er ist eine sehr, sehr alte Technik, die mit älteren Methoden des Datenbankzugriffs in PHP einhergeht. Ab PHP 7 gibt es diese Funktion überhaupt nicht mehr, was uns zu unserer nächsten Lösung führt.
Verwenden von vorbereiteten Anweisungen
Vorbereitete Anweisungen sind eine Methode, Datenbankabfragen sicherer und zuverlässiger zu gestalten. Die Idee dahinter ist, dass wir, anstatt die rohe Abfrage an die Datenbank zu senden, der Datenbank zuerst die Struktur der Abfrage mitteilen, die wir senden wollen. Das meinen wir mit dem „Vorbereiten“ einer Anweisung. Sobald eine Anweisung vorbereitet ist, übergeben wir die Informationen als parametrisierte Eingaben, damit die Datenbank die „Lücken füllen“ kann, indem sie die Eingaben in die zuvor gesendete Abfragestruktur einfügt. Dadurch wird den Eingaben jegliche Sonderrolle entzogen, sodass sie im gesamten Prozess als reine Variablen (oder Nutzdaten, wenn Sie so wollen) behandelt werden. So sehen vorbereitete Anweisungen aus:
<?php $servername = "localhost"; $username = "username"; $password = "password"; $dbname = "myDB"; // Verbindung erstellen $conn = new mysqli($servername, $username, $password, $dbname); // Verbindung prüfen if ($conn->connect_error) { die("Verbindungsfehler: " . $conn->connect_error); } // vorbereiten und binden $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)"); $stmt->bind_param("sss", $firstname, $lastname, $email); // Parameter setzen und ausführen $firstname = "John"; $lastname = "Doe"; $email = "john.doe@example.com"; $stmt->execute(); $firstname = "Mary"; $lastname = "Moe"; $email = "mary.moe@example.com"; $stmt->execute(); $firstname = "Julie"; $lastname = "Dooley"; $email = "julie.dooley@example.com"; $stmt->execute(); echo "Neue Datensätze erfolgreich erstellt"; $stmt->close(); $conn->close(); ?>
Ich weiß, der Prozess mag unnötig kompliziert erscheinen, wenn Sie mit vorbereiteten Anweisungen noch nicht vertraut sind, aber das Konzept ist den Aufwand wert. Hier ist eine gute Einführung dazu.
Für diejenigen, die bereits mit der PDO-Erweiterung von PHP vertraut sind, mit der vorbereitete Anweisungen erstellt werden, habe ich einen kleinen Ratschlag:
Warnung: Vorsicht beim Einrichten von PDO
Wenn wir PDO für den Datenbankzugriff verwenden, können wir uns in einem trügerischen Gefühl von Sicherheit wiegen. „Ah, ich benutze PDO, also brauche ich mir keine weiteren Gedanken zu machen“ – so denken wir im Allgemeinen. Es stimmt, dass PDO (oder vorbereitete MySQLi-Anweisungen) ausreichen, um alle Arten von SQL-Injection-Angriffen zu verhindern, aber Sie müssen bei der Einrichtung vorsichtig sein. Es ist üblich, einfach Code aus Tutorials oder früheren Projekten zu kopieren und einzufügen, aber diese Einstellung kann alles zunichte machen:
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
Diese Einstellung weist PDO an, vorbereitete Anweisungen zu emulieren, anstatt tatsächlich die Funktion für vorbereitete Anweisungen der Datenbank zu verwenden. Folglich sendet PHP einfache Abfragezeichenketten an die Datenbank, selbst wenn Ihr Code so aussieht, als würde er vorbereitete Anweisungen erstellen und Parameter setzen. Mit anderen Worten, Sie sind genauso anfällig für SQL-Injection wie zuvor. 🙂
Die Lösung ist einfach: Stellen Sie sicher, dass diese Emulation auf „false“ gesetzt ist.
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Jetzt ist das PHP-Skript gezwungen, vorbereitete Anweisungen auf Datenbankebene zu verwenden, wodurch alle Arten von SQL-Injection verhindert werden.
Verwendung von WAF zur Prävention
Wussten Sie, dass Sie Webanwendungen auch mit WAFs (Web Application Firewalls) vor SQL-Injection schützen können?
Nun, nicht nur vor SQL-Injection, sondern auch vor vielen anderen Schwachstellen der Schicht 7, wie Cross-Site-Scripting, fehlerhafte Authentifizierung, Cross-Site-Request-Forgery, Datenoffenlegung usw. Sie können entweder selbst gehostete wie ModSecurity oder Cloud-basierte verwenden.
SQL-Injection und moderne PHP-Frameworks
SQL-Injection ist so weit verbreitet, so einfach, so frustrierend und so gefährlich, dass alle modernen PHP-Webframeworks mit eingebauten Gegenmaßnahmen ausgestattet sind. In WordPress haben wir zum Beispiel die Funktion `$wpdb->prepare()`, während ein MVC-Framework die ganze Arbeit für Sie erledigt, sodass Sie nicht einmal daran denken müssen, SQL-Injection zu verhindern. Es ist etwas nervig, dass man in WordPress Statements explizit vorbereiten muss, aber hey, es ist WordPress. 🙂
Wie dem auch sei, mein Punkt ist, dass sich moderne Webentwickler keine Gedanken über SQL-Injection machen müssen und sich dieser Möglichkeit daher gar nicht bewusst sind. Selbst wenn sie in ihrer Anwendung eine Hintertür offen lassen (vielleicht ein $_GET-Abfrageparameter und alte Gewohnheiten, eine unsaubere Abfrage abzuschicken), können die Folgen verheerend sein. Es ist also immer besser, sich die Zeit zu nehmen, tiefer in die Grundlagen einzutauchen.
Fazit
SQL-Injection ist ein sehr unangenehmer Angriff auf eine Webanwendung, der sich jedoch leicht vermeiden lässt. Wie wir in diesem Artikel gesehen haben, reicht es aus, bei der Verarbeitung von Benutzereingaben und der Abfrage der Datenbank vorsichtig zu sein. Allerdings arbeiten wir nicht immer innerhalb der Sicherheitsvorkehrungen eines Webframeworks, daher ist es besser, sich dieser Art von Angriffen bewusst zu sein und nicht darauf hereinzufallen.