Sichere Datenbankzugriffe durch Prepared Statements

03/06/2010 20:37 A7!9#1
Um es gleich zu Anfang zu nennen: Ich werde als Beispiel-Programmier-Sprache PHP wählen, da es mit Abstand am häufigsten dort angewendet werden sollte. Grundsätzlich ist diese Technik auch auf andere Sprachen anwendbar.

1. Etwas Theorie
2. Praktisches
3. Schlusswort

Vorwort:

PHP hatte lange Zeit keine standardisierte Schnittstelle für Datenbanken, um auf verschiedene Datenbanksysteme zuzugreifen, musste der PHP-Entwickler sich viele verschiedene Funktionen merken.
Der Zugriff auf Datenbanken wurde auch nicht in Form einer Klasse angeboten, man für einheitlichen Zugriff auf DBMS war man gezwungen, entweder eine eigene Klasse zu schreiben oder PEAR:B bzw. PEAR::MDB2 zu verwenden.

Mit PDO wird dem Entwickler diese Arbeit nun abgenommen, er kann auch auf PEAR verzichten, was in einen kleinen Performance-Gewinn resultiert. Denn PDO sind keine Scripte sonder eine PECL-Erweiterung, welche für Unix/Linux als .so-Datei und für Windows als .dll gibt, das Parsen entfällt damit.

Im folgenden soll eine kurze Einführung in Prepared Statements mittels PDO Interesse für diese Technologie geben und Basis-Wissen darüber vermitteln.

1. Etwas Theorie

Die Datenbank ist ein wunderbares Medium, um Inhalte in eine dynamische Seite einzubinden. Leider kann dieser Datenbank-Zugriff bei unachtsamer Verwendung zu einer großen Sicherheitslücke werden.

Wir betrachten den folgenden Query, wie er vermutlich in sehr vielen (vor allem selbst-entwickelten) Webseiten zu finden sein wird:

Code:

"SELECT userid FROM users WHERE username = '$username' AND password = '$password'"

Dieses Statement ist hochgradig manipulierbar, im speziellen nennt man diese Manipulationen SQL-Injection. Es basiert auf der einfachen Tatsache, das wir als Webmaster nicht kontrollieren können, was ein Benutzer in das Eingabefeld für seinen Benutzernamen einträgt. Beispielsweise könnte er dort folgendes eintragen:

saftmeister' AND 1=1; --

Was folgendes bewirken würde:

Code:

"SELECT userid FROM users WHERE username = 'saftmeister' AND 1=1 -- ' AND password = ' '"

Wenn man weiß, das alles, was nach dem doppelten Minus-Zeichens als Kommentar angesehen wird, sucht der Query nur nach dem Benutzername, denn 1 ist immer 1.

Dieser Tatsache kann auf verschiedene Weise entgegen gewirkt werden.
- Man verwendet Filter-Techniken (mysql_real_escape_string, Reguläre Ausdrücke und ähnliches) um SQL-Injection zu verhindern.
- Man verwendet PDs

Die Anwendung von manuellen Filtertechniken führt nicht selten zu schwer lesbarem und damit auch schwer wartbarem Code.
PSs haben hier den Vorteil, die Übersichtlichkeit in einem Programm zu erhöhen. Außerdem gibt es noch den netten Nebeneffekt der geringeren Auslastung von CPU, Speicher und Bandbreite, da Queries nicht ständig neu gesendet werden müssen, um ein und die selbe Abfrage mit verschiedenen Daten als Parameter zu senden.

2. Praktisches

Um eine solches Problem in PS zu lösen, wird ein SQL-Server benötigt, der PSs unterstützt (z.B. MySQL, mindestens Version 4.1, die Schnittstelle wird MySQLi genannt), sowie eine Programmiersprache, die PSs senden kann z.B. die PDO von PHP5. Für PHP4 gibt es die Möglichkeit, eine Klasse einzubinden, um die Vorzüge von PSs zu nutzen.

Im folgenden der Code, danach die Erklärung:

Beispiel 1:

PHP Code:
$user "myuser";
$password "mypassword";

$dbc = new PDO("mysql:host=localhost;dbname=mydb"$user$password);

$sql "SELECT userid FROM users WHERE username = :username AND password = :password";
$stmt $dbc->prepare($sql);

$stmt->bindParam':username'$_POST['username'] );
$stmt->bindParam':password'$_POST['password'] );

if(
$stmt->execute())
{
  
$data $stmt->fetch(PDO::FETCH_OBJ);

  echo 
$data->userid;
}
$result null;
$stmt null;
$dbc null
Erklärung:
In der dritten Zeile wird eine Verbindung zum Datenbank-Server mittels der sog. DSN (Data Source Name), einer standardisierten Form, aufgebaut.

Zeile vier zeigt, wie ein Query zusammen gebaut wird, die Besonderheit der PSs werden hier schon sichtbar. Anstatt hier die Werte aus dem Login-Formular einzubinden, arbeitet ein PS mit Platzhaltern, an deren Stelle nach der Prüfung (weiter unten) die Werte automatisch gesetzt werden.

In der darauf folgenden Zeile 5 wird das SQL-Statement vorbereitet, das erledigt die Klassenmethode prepare() für uns. Hier passiert der ganze Trick, das Statement wird an den Server geschickt und dort einmal analysiert und zwischen gespeichert. Dieser Vorgang muss nur einmal durchgeführt werden, nicht wie bei den bekannten mysql_*-Funktionen, bei denen jeder Query einzeln gesendet wird.

Die nächsten Zeilen 6 und 7 beschreiben, wie die Platzhalter im PS mit den gewünschten Werten gefüllt werden. Außerdem filtert (escapen) der SQL-Server hier schon die bestimmten Zeichen, die unter normalen Voraussetzungen ein SQL-Injection verursachen können.

Der eigentliche Query im Cache wird nicht verändert wird. Das bedeutet, das der selbe Query mehrere Male hintereinander verwendet werden kann, ohne das ein neuer gesendet werden muss. Diese Tatsache wirkt sich darin aus, das Bandbreite gespart wird, da nicht mehrere Queries gesendet, sondern immer nur die unterschiedlichen Werte gebunden werden müssen (siehe Beispiel 2).

Zeile 8 schickt ein Signal an den SQL-Server, das der Query nun ausgeführt werden soll und ein Ergebnis erwartet wird, welches in einer Objekt-Variable gespeichert werden soll. Dabei handelt es sich um die komplette Ergebnismenge des Queries nicht um einen einzelnen Datensatz.

In der folgenden Zeile 9 wird nun ein Datensatz aus der Ergebnismenge geholt, was analog zum gewohnten mysql_fetch_* wäre.

Nun kann man in Zeile 10 die ausgelesenen Daten weiter verarbeiten, in diesem Beispiel werden sie ausgegeben.

Die letzten drei Zeilen dienem zum Aufräumen und Abbauen der Verbindung zum Datenbank-Server. Falls persistente Verbindungen erwünscht sind, könne diese mit dem Verbindungsflag PDO::ATTR_PERSISTENT aufgebaut werden:

PHP Code:
  <?php
$dbc 
= new PDO('mysql:host=localhost;dbname=test'$user$pass, array(
  
PDO::ATTR_PERSISTENT => true
));
?>
Beispiel 2: Verwenden des gleichen Queries für mehrere Abfragen

Wie schon oben erwähnt, brauch ein Query, der mehrere gleiche Abfragen erledigen soll, nur einmal vorbereitet werden:

PHP-Code:
PHP Code:
$customer_1 "schmidt";
$customer_2 "müller";
$customer_3 "maier";

$sql "INSERT INTO users (username) VALUES (:username)";

$stmt $dbc->prepare($sql);

$stmt->bindParam(':username'$customer_1);
$stmt->execute();

$stmt->bindParam(':username'$customer_2);
$stmt->execute();

$stmt->bindParam(':username'$customer_3);
$stmt->execute(); 
Dieser Vorgang kann natürlich auch in einer Schleife ausgeführt werden:

PHP Code:
$users = array(
  
'schmidt',
  
'müller',
  
'maier'
);

$sql "INSERT INTO users (username) VALUES (:username)";

$stmt $dbc->prepare($sql);
$stmt->bindParam(':username'$username);
foreach(
$users as $username) {
   
$stmt->execute();

Das letzte Beispiel ist schon etwas komplizierter, hier wird eine Variable an den Query gebunden. Diese Variable wird bei jedem Schleifendurchlauf mit einem neuen Wert versehen und der Query sofort ausgeführt.

3. Schlusswort

Insgesamt sehen die PS etwas komplizierter aus, als der reguläre Zugriff auf die DB, was aber täuscht, wenn man es mit komplizierten Queries zu tun hat. Hier bestechen PSs mit ihrer guten Lesbarkeit. Außerdem wird die Sicherheit enorm gesteigert, man kann einfach nicht mehr vergessen, Zeichen zu escapen. Mit PSs werden durch die geringeren Sende-Volumen Systemresourcen gespart.

Fazit: Bei neuen Projekten immer Prepared Statements verwenden und wenn möglich, bestehende Projekte darauf umarbeiten. Der Arbeitsaufwand wird durch bessere Wartbarkeit auf jeden Fall belohnt.




Quelle:[Only registered and activated users can see links. Click Here To Register...]


Mfg
03/06/2010 20:42 djpromo#2
Versuch mal die ganzen code abschnitte in den [PHP.] ... [/PHP] Block zu packen (ohne den . ) oder auf das PHP symbol klicken , macht das ganze viel übersichtlicher

sieht dann so aus :

PHP Code:
$customer_1 "schmidt";
$customer_2 "müller";
$customer_3 "maier";

$sql "INSERT INTO users (username) VALUES (:username)";

$stmt $dbc->prepare($sql);

$stmt->bindParam(':username'$customer_1);
$stmt->execute();

$stmt->bindParam(':username'$customer_2);
$stmt->execute();

$stmt->bindParam(':username'$customer_3);
$stmt->execute(); 
03/06/2010 20:43 A7!9#3
Gemacht
03/06/2010 22:22 Pand0r#4
Bedanke mich schonmal, kenne zwar alles schon, aber ist schön erklärt.

Liebe Grüße,
pand0r
03/07/2010 09:24 A7!9#5
Quote:
Originally Posted by Pand0r View Post
Bedanke mich schonmal, kenne zwar alles schon, aber ist schön erklärt.

Liebe Grüße,
pand0r
Falls es wer braucht,habs da auf der Seite gesucht und davon hier nix gab wollt ichs public machen