W3docs

Java JDBC PreparedStatement

Parametrisierte SQL-Abfragen sicher in Java mit PreparedStatement ausführen und SQL-Injection verhindern.

Ein PreparedStatement ist ein SQL-Template mit ?-Platzhaltern für die Werte. Die Werte werden separat per Index gesetzt, und der Treiber sendet das Template und die Daten über getrennte Kanäle — so kann ein Wert niemals als SQL interpretiert werden. Das ist die wichtigste Gewohnheit in JDBC: Sie macht Injection strukturell unmöglich und erlaubt der Datenbank, den Query-Plan wiederzuverwenden. Bevorzugen Sie es gegenüber einem einfachen Statement für praktisch alles.

Dieses Kapitel behandelt das Erstellen, Binden und Ausführen eines PreparedStatement, warum es SQL-Injection verhindert, wie NULL- und typisierte Werte gebunden werden und wann die Wiederverwendung eines Statements sich lohnt.

Erstellen, binden, ausführen

String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(url, user, pw);
     PreparedStatement ps = conn.prepareStatement(sql)) {
  ps.setString(1, name);   // bind by 1-based index
  ps.setInt(2, age);
  int rows = ps.executeUpdate();
}

Die Platzhalter werden ab 1 nummeriert, nicht ab 0 — eine häufige Quelle für Off-by-one-Fehler. Jedes setXxx entspricht dem Typ der Spalte: setString, setInt, setBigDecimal, setTimestamp und so weiter.

Warum es Injection verhindert

Bei einem Statement ist der Wert Teil des SQL-Textes, sodass ein Anführungszeichen im Wert das Literal beenden und neue Befehle einschleusen kann. Bei einem PreparedStatement ist das SQL fest und wird vor dem Binden eines Wertes geparst; der Wert wird dann als typisierter Parameter übertragen. Es gibt keinen String, aus dem das Anführungszeichen eines Angreifers ausbrechen könnte — der gefährliche Wert aus dem Statement-Kapitel wird schlicht zu einem wörtlichen Namen.

Nach dem Ausführen einer Abfrage lesen Sie ihre Zeilen mit einem ResultSet, genau wie bei einem Statement.

NULL und spezielle Typen binden

Sie können kein Java-null an setInt übergeben (es erwartet einen primitiven Typ), und setString(i, null) ist für manche Typen mehrdeutig. Die explizite Form ist setNull(index, sqlType), wobei der Spaltentyp aus java.sql.Types angegeben wird:

ps.setNull(3, java.sql.Types.VARCHAR);

Ein PreparedStatement wiederverwenden

Ein PreparedStatement ist darauf ausgelegt, viele Male mit unterschiedlichen Werten ausgeführt zu werden — setzen, ausführen, zurücksetzen, wiederholen. Die Datenbank parst und plant das SQL einmal und verwendet es wieder, weshalb PreparedStatements in einer Schleife auch schneller sind als das wiederholte Erstellen eines Statement-Strings. Für Massen-Inserts kombinieren Sie dies mit Batch-Verarbeitung.

Ein ausgearbeitetes Beispiel: Anatomie eines Templates und seiner Bindungen

Dieses Programm behandelt das SQL-Template als Daten: Es zählt die Platzhalter, durchläuft die Werte, die daran gebunden werden (einschließlich des schädlichen Strings, der das Statement kaputt gemacht hat), und zeigt den setNull-Typcode — all die beweglichen Teile der Parameterbindung, ohne eine Live-Datenbank.

java— editable, runs on the server

Was man aus dem Lauf mitnehmen sollte:

  • Das Template hat drei ?-Platzhalter, und das Programm zählt sie — diese Anzahl gibt genau an, wie viele setXxx-Aufrufe gemacht werden müssen. Eine Abweichung (Parameter 4 einer Abfrage mit 3 Platzhaltern binden) löst bei der Ausführung einen Fehler aus.
  • Bindungen sind 1-basiert: Parameter 1 ist das erste ?. Die Schleife gibt bind 1, bind 2, bind 3 aus, um dies zu verdeutlichen — der häufigste Anfängerfehler ist das Starten bei 0.
  • Der erste Wert ist derselbe x'; DROP TABLE users;---String, der im letzten Kapitel das Statement kompromittiert hat. Hier ist er nur ein Datenwert, der an Parameter 1 gebunden wird; der Treiber speichert ihn wörtlich als Namen. Die Injection wird durch Konstruktion neutralisiert, nicht durch Escaping.
  • Das null bei Parameter 3 ist der Grund, warum setNull(index, Types.VARCHAR) existiert. JDBC benötigt den SQL-Typ, um der Datenbank mitzuteilen, welche Art von NULL es ist — Sie benennen ihn mit einer java.sql.Types-Konstante.
  • Jeder Wert hat einen impliziten Typ — String, int, NULL-of-VARCHAR — weshalb es ein setXxx pro Typ gibt statt eines einzigen stringly-typed Setters. Den Setter dem Spaltentyp anzupassen ist die Disziplin, die PreparedStatements sowohl sicher als auch korrekt hält.

Übungen

Übung
Warum verhindert ein PreparedStatement SQL-Injection, selbst wenn ein gebundener Wert ein einfaches Anführungszeichen und ein Semikolon enthält?
Warum verhindert ein PreparedStatement SQL-Injection, selbst wenn ein gebundener Wert ein einfaches Anführungszeichen und ein Semikolon enthält?
Was this page helpful?