SQL-Injection verhindern

Warum eine der ältesten Web-Sicherheitslücken immer noch vorkommt, wie Prepared Statements sie strukturell verhindern, und welche Sonderfälle auch bei korrekter Umsetzung übersehen werden.

SQL-Injection ist seit über zwei Jahrzehnten bekannt und dennoch regelmäßig Ursache echter Sicherheitsvorfälle. Der Grund liegt selten in fehlendem Wissen um die Grundregel, sondern meist in Sonderfällen, an denen die Grundregel unvollständig angewendet wird - auch in Codebasen, die an den offensichtlichen Stellen bereits korrekt geschützt sind.

1. Die eigentliche Ursache

SQL-Injection entsteht immer dann, wenn Nutzereingaben als Teil einer SQL-Abfrage direkt zusammengesetzt werden, statt sie strikt von der Abfragestruktur getrennt zu behandeln. Fügt eine Anwendung etwa eine Nutzereingabe direkt in eine Zeichenkette wie SELECT * FROM users WHERE name = 'EINGABE' ein, entscheidet allein der Inhalt der Eingabe darüber, wo das schließende Hochkomma landet und was die Datenbank als Code statt als Wert interpretiert. Jede Stelle, an der eine Anwendung Eingaben direkt in eine Abfrage einfügt, ist potenziell angreifbar - unabhängig davon, wie “unwichtig” das betroffene Formular erscheint, und unabhängig davon, ob die Eingabe aus einem sichtbaren Formularfeld, einem URL-Parameter, einem HTTP-Header oder einem Cookie stammt.

2. Die zuverlässige Lösung: Prepared Statements im Detail

Parametrisierte Abfragen, auch Prepared Statements genannt, trennen die feste Struktur einer SQL-Abfrage strikt von den eingesetzten Werten. Technisch läuft das in zwei Schritten ab: Zuerst schickt die Anwendung die Abfragestruktur mit Platzhaltern an die Datenbank, die daraus einen Ausführungsplan erstellt. Erst danach werden die eigentlichen Werte separat übergeben und ausschließlich als Daten in diesen bereits feststehenden Plan eingesetzt - die Datenbank hat zu diesem Zeitpunkt die Struktur der Abfrage längst interpretiert und kann Werte grundsätzlich nicht mehr nachträglich als Code umdeuten, egal welche Zeichen sie enthalten. Praktisch jede moderne Programmiersprache und jedes Datenbank-Framework unterstützt Prepared Statements nativ.

3. Warum einfaches Filtern nicht ausreicht

Ein verbreiteter, aber unzuverlässiger Ansatz ist das manuelle Filtern oder Escapen “gefährlicher” Zeichen wie Hochkommata. Dieser Ansatz ist fehleranfällig, da Escaping-Regeln je nach Datenbanksystem, Zeichenkodierung und Kontext (etwa innerhalb eines LIKE-Musters mit Wildcards) unterschiedlich sind und leicht Sonderfälle übersehen werden. Prepared Statements umgehen dieses Problem grundsätzlich, da sie gar nicht erst versuchen, gefährliche Eingaben zu erkennen oder zu neutralisieren, sondern strukturell verhindern, dass Eingaben überhaupt als Code interpretiert werden können.

4. Grenzen von Prepared Statements

Prepared Statements schützen zuverlässig Werte - aber nicht Bestandteile der Abfragestruktur selbst. Tabellennamen, Spaltennamen oder die Sortierrichtung (ASC/DESC) lassen sich technisch nicht als parametrisierter Wert übergeben, da sie Teil der Abfragestruktur sind, nicht der Daten. Muss eine Anwendung solche Bestandteile dynamisch aus Nutzereingaben bestimmen - etwa bei einer frei wählbaren Sortierspalte in einer Tabellenansicht -, ist strikte Validierung gegen eine feste Liste erlaubter Werte (eine Allowlist) nötig, da hier keine Parametrisierung greift.

5. ORMs schützen nicht automatisch vor jeder Query

Objektrelationale Mapper (ORMs) verwenden für ihre Standard-Methoden in aller Regel intern Prepared Statements und sind entsprechend sicher. Die meisten ORMs bieten jedoch zusätzlich einen “Escape-Hatch” für Rohabfragen (Raw Queries) an, um Fälle abzudecken, die sich mit der ORM-eigenen Abfragesprache nicht ausdrücken lassen. Wird an dieser Stelle wieder eine Nutzereingabe per Zeichenkettenverkettung eingebaut, entsteht dieselbe Schwachstelle wie ohne ORM - das ORM selbst schützt nur, solange ausschließlich seine parametrisierten Standardmethoden verwendet werden.

6. Stored Procedures sind kein automatischer Schutz

Auch innerhalb einer gespeicherten Prozedur (Stored Procedure) kann SQL-Injection entstehen, wenn die Prozedur selbst Eingabeparameter per Zeichenkettenverkettung zu einer dynamisch ausgeführten Abfrage zusammensetzt (in vielen Datenbanksystemen etwa über einen expliziten “Execute Dynamic SQL”-Befehl). Eine Stored Procedure ist für sich genommen kein Sicherheitsmerkmal - entscheidend ist wie bei Anwendungscode, ob innerhalb der Prozedur Werte parametrisiert oder als Text zusammengebaut werden.

7. Second-Order und Blind SQL-Injection

Nicht jede SQL-Injection zeigt sich unmittelbar. Bei Second-Order-Injection wird eine schädliche Eingabe zunächst unauffällig und scheinbar sicher gespeichert (etwa ein Nutzername), aber später an anderer Stelle im System ungeschützt in eine weitere Abfrage eingebaut - der eigentliche Angriff wird also zeitversetzt und an anderer Stelle wirksam als die ursprüngliche Eingabe. Bei Blind SQL-Injection erhält der Angreifer keine direkte Fehlermeldung oder Datenausgabe, kann aber über das Verhalten der Anwendung (etwa unterschiedliche Antwortzeiten oder ob eine Seite normal oder mit Fehler lädt) systematisch Informationen aus der Datenbank ableiten. Beide Varianten unterstreichen, warum “es kommt keine sichtbare Fehlermeldung” kein verlässliches Zeichen für Sicherheit ist.

8. Zusätzliche Schutzebenen

Über Prepared Statements hinaus gehören das Prinzip minimaler Datenbankrechte (die Anwendung sollte nur auf tatsächlich benötigte Tabellen und Operationen zugreifen können, nicht mit administrativen Rechten verbinden) und eine Web Application Firewall zu den empfohlenen zusätzlichen Schutzmaßnahmen. Eine WAF kann bekannte Angriffsmuster erkennen und blockieren, bevor sie die Anwendung überhaupt erreichen, ersetzt aber nicht die korrekte Implementierung parametrisierter Abfragen im Anwendungscode selbst - sie ist eine zusätzliche Verteidigungsebene (Defense in Depth), kein Ersatz für sicheren Code.

9. Häufige Fehler

Ein häufiger Fehler ist die Annahme, ein einmal auf Prepared Statements umgestelltes Projekt sei dauerhaft geschützt, obwohl neue Features im Zeitverlauf wieder Zeichenkettenverkettung einführen - insbesondere über ORM-Raw-Queries oder dynamisch erzeugte SQL-Fragmente für Sortierung und Filterung. Ein zweiter häufiger Fehler ist, Eingabevalidierung (etwa Längenbegrenzung oder Formatprüfung) mit Injection-Schutz zu verwechseln: Validierung verbessert die Datenqualität, verhindert aber für sich allein keine Injection, wenn die eigentliche Abfrage weiterhin unparametrisiert zusammengebaut wird. Ein dritter Fehler ist, Fehlermeldungen der Datenbank unverändert an den Nutzer weiterzugeben - das erleichtert Angreifern das gezielte Ausloten einer Schwachstelle, selbst wenn diese durch Prepared Statements eigentlich bereits verhindert wird.

10. Wie du das prüfst

Der verlässlichste Ausgangspunkt für eine Codeprüfung ist die gezielte Suche nach Stellen, an denen SQL-Abfragen als Zeichenketten mit direkt eingefügten Variablen zusammengebaut werden - etwa über String-Konkatenation oder Interpolation statt über Platzhalter. Statische Codeanalyse-Werkzeuge können solche Muster automatisiert markieren, ersetzen aber keine manuelle Durchsicht der Stellen, an denen dynamisches SQL (etwa für Sortierung oder Tabellennamen) bewusst notwendig ist. Für einen Blackbox-Test der eigenen, selbst betriebenen Anwendung lässt sich an Eingabefeldern testweise ein einzelnes Hochkomma eingeben - löst das bereits einen sichtbaren Datenbankfehler statt einer normalen Antwort oder Fehlerbehandlung aus, ist das ein starkes Warnsignal für unparametrisierte Abfragen.

11. Entscheidungsrahmen

  1. Wird eine Nutzereingabe (Formular, URL-Parameter, Header, Cookie) in eine SQL-Abfrage eingesetzt? → Grundsätzlich als potenziell angreifbare Stelle behandeln, unabhängig von der wahrgenommenen Wichtigkeit.
  2. Handelt es sich um einen Wert (Vergleich, Einfügen, Aktualisieren)? → Prepared Statement bzw. parametrisierte ORM-Methode verwenden.
  3. Handelt es sich um einen Bestandteil der Abfragestruktur (Tabellenname, Spaltenname, Sortierrichtung)? → Gegen eine feste Allowlist erlaubter Werte validieren, da Parametrisierung hier nicht greift.
  4. Wird an dieser Stelle eine ORM-Raw-Query oder dynamisches SQL innerhalb einer Stored Procedure verwendet? → Besonders genau prüfen, da der übliche ORM-Schutz hier nicht automatisch greift.
  5. Ist die Implementierung bereits korrekt umgesetzt? → Zusätzlich minimale Datenbankrechte und eine WAF als ergänzende Schutzebenen einplanen, nicht als Ersatz.

Weiterführend