Sicheres Programmieren mit PHP (4)
SQL Injection
PHP eignet sich hervorragend, um mit WWW-Seiten Daten aus Datenbanken
anzuzeigen oder zu ändern.
Aber auch hier gilt es, Sicherheitsaspekte zu beachten, will man nicht böse
Überraschungen erleben.
SQL Injection ist eine Technik, mit der böswillige Angreifer über WWW
SQL-Kommandos erstellen oder existierende verändern, um versteckte Daten sichtbar
zu machen, zu verändern oder zu löschen.
Betrachten wir wieder ein schlechtes Beispiel und ein Angriffs-Szenario:
<?php
#Datenbankabfrage an Hand einer ID aus dem URL
$id = $_REQUEST['id'];
$result = mysql_query("SELECT * from tabelle WHERE id=$id");
# Anzeige des Ergebnisse ...
?>
"Erwartet" wird ein URL der Form
.../script.php?id=42, wodurch aus der
Datenbank der Tabelleneintrag mit der id=42 gelesen wird.
Was aber, wenn der URL so geschrieben wird:
.../script.php?id=42+or+1=1
Die Pluszeichen wandelt der WWW-Server in Leerzeichen um, und der arglose
mysql_query -Aufruf führt plötzlich dazu, dass alle (mglw. sensiblen) Daten ausgelesen werden:
SELECT * from tabelle WHERE id=42 or 1=1
Auch in diesen Fall erinnern wir uns an den Merksatz:
Wir müssen
alle Eingabewerte überprüfen, auch wenn das etwas Mühe macht.
PHP hält dafür aber eine Menge an Möglichkeiten bereit.
Wird als Wert eine Integer-Zahl erwartet, so können Sie dies prüfen mit der
Funktion
is_numeric() oder Sie setzen den Typ mittels
settype() explizit auf
integer:
<?php
#Datenbankabfrage an Hand einer ID aus dem URL
$id = $_REQUEST['id'];
settype($id, 'integer'); # Zwangsumwandlung in Zahl
$result = mysql_query("SELECT * from tabelle WHERE id=$id");
# Anzeige des Ergebnisse ...
}
?>
Werden als externe Parameter Zeichenketten erwartet, gerät die Prüfung meist
etwas aufwändiger.
Hier ist eine Prüfung auf plausible Werte angebracht.
Zumindest sollten Sie Zeichen kodieren, die für SQL eine Sonderbedeutung
haben: Semikolon, einfacher und doppelter Anführungsstrich usw.
Dafür bietet sich die PHP-Funktion
addslashes() an.
<?php
#Datenbankabfrage an Hand eines eingetippten Suchwortes
$suchwort = $_REQUEST['wort'];
# Entferne rigoros alle Semikolons
$suchwort = ereg_replace(';', '', $suchwort);
if (get_magic_quotes_gpc() == 0) {
# Falls die System-PHP-Einstellungen es noch nicht automatisch tun,
# kodiert die Funktion addslashes weitere SQL-Sonderzeichen:
$suchwort = addslashes($suchwort);
}
$result = mysql_query("SELECT * from tabelle WHERE name like \"$suchwort\"");
# Anzeige des Ergebnisse ...
}
?>
Am besten ist es, Sie lehnen dubios erscheinende Abfragen von vornherein ab:
<?php
#Datenbankabfrage an Hand eines eingetippten Suchwortes
$suchwort = $_REQUEST['wort'];
$laenge = strlen($suchwort);
# Lehne zu kurze/lange oder ein Semikolon enthaltende Suchwörter ab:
if ($laenge <= 0 || $laenge > 100 || ereg(';', $suchwort)) {
print "Fehler im Suchwort ...";
exit;
}
?>
Die Programmier-Sorgfalt muss noch größer sein, wenn die Datenbank via PHP
auch geändert werden soll. Bei
INSERT- und
UPDATE-
Operationen muss sichergestellt werden, dass plausible Werte eingefügt werden.
Besondere Vorsicht ist natürlich bei
DELETE-Operationen usw. nötig.
Für größere PHP-Projekte mit Datenbank-Zugriff empfiehlt sich auch aus
Sicherheitsaspekten die Einarbeitung in Datenbank-Abstraktionspakete,
wie das
PEAR-Paket MDB2
oder - ab PHP 5.1 -
PDO (PHP Data Objects):
- Große Unabhängigkeit von der zugrunde liegenden Datenbank
- Objektorientierter Ansatz
- Sicherheit: Vorgefertigte Abfragen (prepared statements) mit Platzhaltern, eingesetzte Werte werden automatisch sicher "geflüchtet"
<form action="d1.php">
ID: <input name="id" size="4" />
<input type="submit" value="Senden" />
</form>
<?php
if (! isset($_REQUEST['id'])) return; # id nicht gesetzt: Schluss
require_once("MDB2.php"); # PEAR MDB2 einbinden
$db = MDB2::factory('mysql://@www-db.tu-chemnitz.de/events');
# $db = MDB2::factory('mysql://benutzername:passwort@server/datenbank');
if (PEAR::isError($db)) { die ($db->getMessage()); }
# Vorbereiten mit Platzhaltern und Typ
$db_q = $db->prepare('SELECT titel FROM event WHERE id=?', array('integer'));
# Ausführen
$res = $db_q->execute($_REQUEST['id']); # ziemlich ungefährlich
if (PEAR::isError($res)) { # Fehler?
die($res->getMessage());
}
#Ergebnisse abfragen
while (($row = $res->fetchRow())) {
echo 'Titel: ' . htmlspecialchars($row[0]) . "<br />\n";
}
?>
Vor einer Abfrage von Daten muss zunächst die Verbindung zur Datenbank hergestellt
werden, wozu neben Servernamen auch der Datenbank-Nutzer und das Passwort
anzugeben ist, z.B. für MySQL:
<?php
#Datenbank-Verbindung herstellen:
# das @ vorm Funktionsaufruf verhindert die eingebaute Fehlermeldung
$db = @mysql_connect('mysql.hrz.tu-chemnitz.de', 'dbnutzer', 'geheim')
or die ("Keine Verbindung zur Datenbank möglich");
?>
Hier haben wir das Problem, dass das Passwort des Datenbank-Benutzers im
PHP-Skript stehen muss. Das birgt natürlich eine Gefahr, wenn es in falsche
Hände gelangt.
Als Grundregel beachten Sie:
Verwenden Sie einen Datenbank-Benutzer, dessen Rechte entsprechend Ihrer
Anwendung limitiert sind.
Wenn Sie via PHP-Skript nur SELECT-Anweisungen ausführen, verwenden Sie den
DB-Nutzer, der nur leseberechtigt ist.
Mit einem
speziellen Verfahren steht Web-Programmierern auf den zentralen Webservern der TU
ein sicherer Weg zur Verfügung, solche Geheimnisse in Webanwendungen zu
verwenden, ohne dass diese im zugänglichen Dateisystem abgelegt werden müssen.
Für Anwendungen und Projekte mit höheren Sicherheitsanforderungen sind
dedizierte WWW-Server vorzusehen.
Das URZ bietet dafür sog.
PROWeb-Server an.
Weitere Hinweise:
http://www.tu-chemnitz.de/docs/php/security.database.html