Das klassische Argument für den Einsatz parametrisierter Statements ist der Schutz vor SQL-Injection. SQL-Injection bezeichnet dabei den Vorgang des Einschleusens von fremden SQL-Statements in eigene SQL-Statements.
Beispiel: Die klassische Benutzer-Authentifizierung, bei der Benutzername und Kennwort übergeben werden. Der typische (C#-) Code für das generieren des SQL-Statements sieht dann in etwa so aus:
// Werte einlesen
String username = tbLoginName.Text;
String password = tbPassword.Text;
// Statement bauen
String sqlStatement = “SELECT COUNT(1) FROM Users ” +
“WHERE Username = ‘” + username + “‘ ” +
“AND Password = ‘” + password + “‘”;
// Command erzeugen
DbCommand command = conn.CreateCommand();
command.CommandText = sqlStatement;
// Statement ausführen
int result = (int) command.ExecuteScalar();
Lassen Sie uns mal einige Varianten für Benutzernamen und Kennwörter durchspielen.
Variante #1:
- Benutzername: foo
- Kennwort bla
- SQL-Statement: SELECT COUNT(1) FROM Users
WHERE Username = ‘foo‘ AND Password = ‘bla‘
Kein Problem hier, alles gut. Wenn Benutzername und Kennwort passen, dann ist es fein.
Variante #2:
- Benutzername: ‘ OR 1=1 –
- Kennwort: Weiß nicht
- SQL-Statement: SELECT COUNT(1) FROM Users
WHERE Username = ‘‘ OR 1=1 –‘ AND Password = ‘Weiß nicht‘
Großes Problem hier. Das SQL-Statement sieht irgendwie krank aus und kann ganz sicher nicht funktionieren. Sicher?! Doch, es funktioniert, denn die beiden aufeinanderfolgenden Bindestriche interpretiert ein SQL-Server beispielsweise als Kommentar – alles, was danach kommt, wird also ignoriert. Das tatsächlich ausgeführte SQL-Statement sieht also so aus:
- SQL-Statement: SELECT COUNT(1) FROM Users WHERE Username = ” OR 1=1
Ganz gewaltiges Problem jetzt: Die Abfrage selektiert alle Datensätze, deren Username-Spalte entweder leer ist, oder für die 1=1 gilt. Und da 1=1 immer gilt, werden also alle Datensätze selektiert. Die Anmeldung wird jetzt also klappen.
Variante #3:
- Benutzername: ‘; INSERT INTO Users (‘karsten’, ‘sicher’) –
- Kennwort: Keine Ahnung
- SQL-Statement: SELECT COUNT(1) FROM Users
WHERE Username = ‘‘; INSERT INTO Users (‘karsten’, ‘sicher’) –‘ AND Password = ‘Keine Ahnung‘
Ebenfalls großes Problem: Nun werden zwei SQL-Statements ausgeführt, denn das Semikolon trennt SQL-Statements voneinander. Alles, was nach dem Kommentar-Zeichen kommt, wird komplett ignoriert. Das eigentliche SQL-Statement sieht also so aus:
- SQL-Statement: SELECT COUNT(1) FROM Users WHERE Username = ”; INSERT INTO Users (‘karsten’, ‘sicher’)
Super, jetzt haben wir einen zweiten Datensatz in der Tabelle!
Die Lösung
Der Lösungsansatz ist ganz einfach: Parametrisierte SQL-Statements verwenden. Dazu muss der obenstehende Code nur ein wenig abgewandelt werden:
// Werte einlesen
String username = tbLoginName.Text;
String password = tbPassword.Text;
// Statement bauen
String sqlStatement = “SELECT COUNT(1) FROM Users ” +
“WHERE Username = @Username ” +
“AND Password = @Password”;
// Command erzeugen
DbCommand command = conn.CreateCommand();
command.CommandText = sqlStatement;
// Parameter anfügen
DbParameter param = command.CreateParameter();
param.ParameterName = “@Username”;
param.Value = username;
command.Parameters.Add(param);
param = command.CreateParameter();
param.ParameterName = “@Password”;
param.Value = password;
command.Parameters.Add(param);
// Statement ausführen
int result = (int) command.ExecuteScalar();
Nun werden die Daten anders an die Datenbank übergeben – nämlich in zwei Schritten: Zuerst wird das Statement übergeben, anschließend erfolgt die Übergabe der Parameter. Auf Ebene der Datenbank wird nun nicht etwa ein SQL-Statement dynamisch zusammengebaut, sondern es wird wie in Form eines Prozeduraufrufs verarbeitet. Für die Beispiele gilt also:
Variante #1:
- Benutzername: foo
- Kennwort bla
- SQL-Statement: SELECT COUNT(1) FROM Users
WHERE Username = @Username AND Password = @Password
Kein Problem hier, alles gut. Wenn Benutzername und Kennwort passen, dann ist es fein.
Variante #2:
- Benutzername: ‘ OR 1=1 –
- Kennwort: Weiß nicht
- SQL-Statement: SELECT COUNT(1) FROM Users
WHERE Username = @Username AND Password = @Password
Kein Problem hier. Wenn es nicht zufällig einen Datensatz mit dem Benutzernamen ‘ OR 1=1 – und dem Kennwort Weiß nicht gibt, dann wird nix gefunden. Und wenn es den Datensatz gibt, ist der Benutzer ordnungsgemäß authentifiziert.
Variante #3:
- Benutzername: ‘; INSERT INTO Users (‘karsten’, ‘sicher’) –
- Kennwort: Keine Ahnung
- SQL-Statement: SELECT COUNT(1) FROM Users
WHERE Username = @Username AND Password = @Password
Kein Problem hier. Wenn es nicht zufällig einen Datensatz mit demBenutzernamen ‘; INSERT INTO Users (‘karsten’, ‘sicher’) – und dem Kennwort Keine Ahnung gibt, dann wird nix gefunden. Und wenn es den Datensatz gibt, ist der Benutzer ordnungsgemäß authentifiziert.
Also, keine Sicherheitsprobleme bei Verwendung von parametrisierten Statements.
Wer mehr über SQL-Injection wissen möchte, sollte dringend einen Blick in die Wikipedia werfen.